@reventlessdev/reventless-seed-aws 1.0.0-alpha.10
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 +69 -0
- package/LICENSE +202 -0
- package/lib/bs/.compiler.log +2 -0
- package/lib/bs/.sourcedirs.json +1 -0
- package/lib/bs/build.ninja +0 -0
- package/lib/bs/compiler-info.json +8 -0
- package/lib/bs/src/ReventlessSeedAws.ast +0 -0
- package/lib/bs/src/ReventlessSeedAws.cmi +0 -0
- package/lib/bs/src/ReventlessSeedAws.cmj +0 -0
- package/lib/bs/src/ReventlessSeedAws.cmt +0 -0
- package/lib/bs/src/ReventlessSeedAws.res +328 -0
- package/lib/bs/src/ReventlessSeedAws.res.mjs +309 -0
- package/lib/bs/src/ReventlessSeedAws_Reconcile.ast +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reconcile.cmi +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reconcile.cmj +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reconcile.cmt +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reconcile.res +389 -0
- package/lib/bs/src/ReventlessSeedAws_Reconcile.res.mjs +318 -0
- package/lib/bs/src/ReventlessSeedAws_Reset.ast +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reset.cmi +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reset.cmj +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reset.cmt +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reset.res +948 -0
- package/lib/bs/src/ReventlessSeedAws_Reset.res.mjs +861 -0
- package/lib/bs/tests/EndpointResolutionTest.ast +0 -0
- package/lib/bs/tests/EndpointResolutionTest.cmi +0 -0
- package/lib/bs/tests/EndpointResolutionTest.cmj +0 -0
- package/lib/bs/tests/EndpointResolutionTest.cmt +0 -0
- package/lib/bs/tests/EndpointResolutionTest.res +83 -0
- package/lib/bs/tests/EndpointResolutionTest.res.mjs +115 -0
- package/lib/bs/tests/ObjectStoreReconcileTest.ast +0 -0
- package/lib/bs/tests/ObjectStoreReconcileTest.cmi +0 -0
- package/lib/bs/tests/ObjectStoreReconcileTest.cmj +0 -0
- package/lib/bs/tests/ObjectStoreReconcileTest.cmt +0 -0
- package/lib/bs/tests/ObjectStoreReconcileTest.res +84 -0
- package/lib/bs/tests/ObjectStoreReconcileTest.res.mjs +102 -0
- package/lib/bs/tests/ObjectStoreResetTest.ast +0 -0
- package/lib/bs/tests/ObjectStoreResetTest.cmi +0 -0
- package/lib/bs/tests/ObjectStoreResetTest.cmj +0 -0
- package/lib/bs/tests/ObjectStoreResetTest.cmt +0 -0
- package/lib/bs/tests/ObjectStoreResetTest.res +170 -0
- package/lib/bs/tests/ObjectStoreResetTest.res.mjs +211 -0
- package/lib/ocaml/.compiler.log +2 -0
- package/lib/ocaml/EndpointResolutionTest.ast +0 -0
- package/lib/ocaml/EndpointResolutionTest.cmi +0 -0
- package/lib/ocaml/EndpointResolutionTest.cmj +0 -0
- package/lib/ocaml/EndpointResolutionTest.cmt +0 -0
- package/lib/ocaml/EndpointResolutionTest.res +83 -0
- package/lib/ocaml/ObjectStoreReconcileTest.ast +0 -0
- package/lib/ocaml/ObjectStoreReconcileTest.cmi +0 -0
- package/lib/ocaml/ObjectStoreReconcileTest.cmj +0 -0
- package/lib/ocaml/ObjectStoreReconcileTest.cmt +0 -0
- package/lib/ocaml/ObjectStoreReconcileTest.res +84 -0
- package/lib/ocaml/ObjectStoreResetTest.ast +0 -0
- package/lib/ocaml/ObjectStoreResetTest.cmi +0 -0
- package/lib/ocaml/ObjectStoreResetTest.cmj +0 -0
- package/lib/ocaml/ObjectStoreResetTest.cmt +0 -0
- package/lib/ocaml/ObjectStoreResetTest.res +170 -0
- package/lib/ocaml/ReventlessSeedAws.ast +0 -0
- package/lib/ocaml/ReventlessSeedAws.cmi +0 -0
- package/lib/ocaml/ReventlessSeedAws.cmj +0 -0
- package/lib/ocaml/ReventlessSeedAws.cmt +0 -0
- package/lib/ocaml/ReventlessSeedAws.res +328 -0
- package/lib/ocaml/ReventlessSeedAws_Reconcile.ast +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reconcile.cmi +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reconcile.cmj +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reconcile.cmt +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reconcile.res +389 -0
- package/lib/ocaml/ReventlessSeedAws_Reset.ast +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reset.cmi +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reset.cmj +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reset.cmt +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reset.res +948 -0
- package/package.json +32 -0
- package/rescript.json +29 -0
- package/src/ReventlessSeedAws.res +328 -0
- package/src/ReventlessSeedAws.res.mjs +309 -0
- package/src/ReventlessSeedAws_Reconcile.res +389 -0
- package/src/ReventlessSeedAws_Reconcile.res.mjs +318 -0
- package/src/ReventlessSeedAws_Reset.res +948 -0
- package/src/ReventlessSeedAws_Reset.res.mjs +861 -0
- package/tests/EndpointResolutionTest.res +83 -0
- package/tests/EndpointResolutionTest.res.mjs +115 -0
- package/tests/ObjectStoreReconcileTest.res +84 -0
- package/tests/ObjectStoreReconcileTest.res.mjs +102 -0
- package/tests/ObjectStoreResetTest.res +170 -0
- package/tests/ObjectStoreResetTest.res.mjs +211 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
// Offline reconciliation for a deployed object store: what is in the store,
|
|
2
|
+
// what the event log actually references, and the difference between them.
|
|
3
|
+
//
|
|
4
|
+
// This is the evidence that makes enabling a store's expiry rule safe — the one
|
|
5
|
+
// mechanism that can *prove* an object is unreferenced, which is why it is an
|
|
6
|
+
// operator-run report and not a request handler. Read-only from start to finish:
|
|
7
|
+
// it lists, it scans, it prints. Nothing here deletes anything, and it needs
|
|
8
|
+
// none of the reset tool's fail-closed gates because there is nothing to fail
|
|
9
|
+
// closed about.
|
|
10
|
+
//
|
|
11
|
+
// The question it answers is the one the lifecycle rule is about to answer
|
|
12
|
+
// destructively: **is every object still tagged pending genuinely unreferenced?**
|
|
13
|
+
// A single row in the "referenced but still tagged" section is a bug caught on
|
|
14
|
+
// a report instead of on a page.
|
|
15
|
+
//
|
|
16
|
+
// ## Why it does not read the `@storageRef` declarations
|
|
17
|
+
//
|
|
18
|
+
// The claim component is declaration-driven: it untags the refs that annotated
|
|
19
|
+
// fields carry. If reconciliation read the same declarations it would inherit
|
|
20
|
+
// the same blind spot, and the two would agree precisely where they were both
|
|
21
|
+
// wrong — a field nobody annotated holds a ref, the claimer never strips its
|
|
22
|
+
// tag, and a report derived from the same list would call the object
|
|
23
|
+
// unreferenced too. So this walks each event's payload for *any* string under
|
|
24
|
+
// the store's prefix.
|
|
25
|
+
//
|
|
26
|
+
// That is deliberately generous, and generous is the safe direction here: it can
|
|
27
|
+
// only ever move an object from "unreferenced" to "referenced", never the
|
|
28
|
+
// reverse. An unannotated ref therefore shows up exactly where it should — in
|
|
29
|
+
// the referenced-but-tagged section, as the thing to fix before turning expiry
|
|
30
|
+
// on.
|
|
31
|
+
//
|
|
32
|
+
// ## Cost
|
|
33
|
+
//
|
|
34
|
+
// A full scan of every event log table and a tagging read per object. Minutes on
|
|
35
|
+
// a demo store, longer on a real one, and both are fine for something an
|
|
36
|
+
// operator runs deliberately before a one-way decision.
|
|
37
|
+
|
|
38
|
+
open ReventlessSeed
|
|
39
|
+
|
|
40
|
+
module Ddb = AwsSdk.DynamoDb
|
|
41
|
+
module Rgt = AwsSdk.ResourceGroupsTaggingApi
|
|
42
|
+
module S3 = AwsSdk.S3
|
|
43
|
+
|
|
44
|
+
// The tag the mint side writes and the claim component removes, restated rather
|
|
45
|
+
// than imported: the seed packages deliberately depend on no framework package —
|
|
46
|
+
// they drive a platform through its public API — so `Upload_PendingTag.key` is
|
|
47
|
+
// out of reach here.
|
|
48
|
+
//
|
|
49
|
+
// That makes this a duplicated constant, and the honest thing is to say what
|
|
50
|
+
// happens if it drifts rather than to claim it cannot. Reading the wrong key
|
|
51
|
+
// would report every object as untagged: a clean bill of health for a store this
|
|
52
|
+
// tool had not actually inspected. `print` therefore calls out the shape that
|
|
53
|
+
// drift produces — objects present, none of them tagged — instead of leaving it
|
|
54
|
+
// as a silent pass. Changing this string means changing `Upload_PendingTag.key`
|
|
55
|
+
// in reventless-aws, and vice versa.
|
|
56
|
+
let pendingTagKey = "reventless:pending"
|
|
57
|
+
|
|
58
|
+
// ── Collecting refs from the event log ───────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
/** Every string under `/{prefix}/` anywhere in a committed event's payload.
|
|
61
|
+
|
|
62
|
+
Recursive because an event's payload is whatever its schema says: a ref can
|
|
63
|
+
sit at the top level, inside an array, or nested in a record. Depth costs
|
|
64
|
+
nothing here and missing a ref costs an object. */
|
|
65
|
+
let rec collectRefs = (~prefix: string, json: JSON.t, into: Set.t<string>): unit =>
|
|
66
|
+
switch json {
|
|
67
|
+
| String(s) =>
|
|
68
|
+
if s->String.startsWith(`/${prefix}/`) {
|
|
69
|
+
into->Set.add(s)
|
|
70
|
+
}
|
|
71
|
+
| Array(items) => items->Array.forEach(item => collectRefs(~prefix, item, into))
|
|
72
|
+
| Object(fields) => fields->Dict.valuesToArray->Array.forEach(v => collectRefs(~prefix, v, into))
|
|
73
|
+
| _ => ()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Scan one table for committed events and collect the refs they carry.
|
|
77
|
+
|
|
78
|
+
Both the aggregate and the DCB event log store a row as
|
|
79
|
+
`{event: "<Type>", data: {…}}`, so a row without an `event` attribute is not
|
|
80
|
+
an event — a snapshot, a DCB fence row, or a read-model row in a table that
|
|
81
|
+
is not an event log at all. That single check is what lets this scan every
|
|
82
|
+
discovered table without having to know which ones are event logs. */
|
|
83
|
+
let refsInTable = async (~table: string, ~prefix: string, into: Set.t<string>): int => {
|
|
84
|
+
let rec loop = async (start: option<dict<JSON.t>>, rows: int): int => {
|
|
85
|
+
let out = await Ddb.DocumentClient.ScanCommand.send(
|
|
86
|
+
Ddb.DocumentClient.ScanCommand.make({tableName: table, exclusiveStartKey: ?start}),
|
|
87
|
+
)
|
|
88
|
+
let items = out.items->Option.getOr([])
|
|
89
|
+
items->Array.forEach(item =>
|
|
90
|
+
switch item->JSON.Decode.object {
|
|
91
|
+
| Some(row) =>
|
|
92
|
+
switch (row->Dict.get("event"), row->Dict.get("data")) {
|
|
93
|
+
| (Some(JSON.String(_)), Some(data)) => collectRefs(~prefix, data, into)
|
|
94
|
+
| _ => ()
|
|
95
|
+
}
|
|
96
|
+
| None => ()
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
let rows = rows + items->Array.length
|
|
100
|
+
switch out.lastEvaluatedKey {
|
|
101
|
+
| Some(k) => await loop(Some(k), rows)
|
|
102
|
+
| None => rows
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
await loop(None, 0)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── Collecting objects from the store ────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
/** One object in the store, and whether it is still provisional. */
|
|
111
|
+
type object = {
|
|
112
|
+
key: string,
|
|
113
|
+
/** The ref an event would carry for this object — the key with a leading `/`,
|
|
114
|
+
which is exactly what the presign service minted. */
|
|
115
|
+
ref: string,
|
|
116
|
+
pending: bool,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let listObjects = async (~bucket: string, ~prefix: string): array<object> => {
|
|
120
|
+
let keys = []
|
|
121
|
+
let rec loop = async (keyMarker, versionMarker): unit => {
|
|
122
|
+
let out = await S3.ListObjectVersionsCommand.send(
|
|
123
|
+
S3.ListObjectVersionsCommand.make({
|
|
124
|
+
bucket,
|
|
125
|
+
prefix: `${prefix}/`,
|
|
126
|
+
keyMarker: ?keyMarker,
|
|
127
|
+
versionIdMarker: ?versionMarker,
|
|
128
|
+
}),
|
|
129
|
+
)
|
|
130
|
+
// Current objects only. Store buckets are created with versioning off, so
|
|
131
|
+
// in practice every version is the latest one; filtering keeps this honest
|
|
132
|
+
// if a deployment ever turns versioning on.
|
|
133
|
+
out.versions
|
|
134
|
+
->Option.getOr([])
|
|
135
|
+
->Array.forEach(v =>
|
|
136
|
+
if v.isLatest {
|
|
137
|
+
keys->Array.push(v.key)
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
switch (out.isTruncated, out.nextKeyMarker) {
|
|
141
|
+
| (Some(true), Some(_)) => await loop(out.nextKeyMarker, out.nextVersionIdMarker)
|
|
142
|
+
| _ => ()
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
await loop(None, None)
|
|
146
|
+
|
|
147
|
+
// One tagging read per object. Sequential on purpose — this competes with a
|
|
148
|
+
// live claim component for the same objects' tagging reads, and an operator
|
|
149
|
+
// report has no deadline worth throttling production for.
|
|
150
|
+
let objects = []
|
|
151
|
+
await keys->Array.reduce(Promise.resolve(), (acc, key) =>
|
|
152
|
+
acc->Promise.then(async _ => {
|
|
153
|
+
let tagging = await S3.GetObjectTaggingCommand.send(
|
|
154
|
+
S3.GetObjectTaggingCommand.make({bucket, key}),
|
|
155
|
+
)
|
|
156
|
+
let pending =
|
|
157
|
+
tagging.tagSet->Option.getOr([])->Array.some(t => t.key == pendingTagKey)
|
|
158
|
+
objects->Array.push({key, ref: `/${key}`, pending})
|
|
159
|
+
})
|
|
160
|
+
)
|
|
161
|
+
objects
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── The report ───────────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
/** The four populations, which are the whole point. Only one of them is a
|
|
167
|
+
problem, and it is the one the lifecycle rule would delete. */
|
|
168
|
+
type report = {
|
|
169
|
+
store: string,
|
|
170
|
+
bucket: string,
|
|
171
|
+
prefix: string,
|
|
172
|
+
eventRows: int,
|
|
173
|
+
/** Referenced by a committed event and STILL TAGGED PENDING. Enabling expiry
|
|
174
|
+
would delete these. Must be empty before a store turns the rule on. */
|
|
175
|
+
referencedButPending: array<object>,
|
|
176
|
+
/** Tagged pending and referenced by nothing — what the rule is for. */
|
|
177
|
+
unreferencedPending: array<object>,
|
|
178
|
+
/** Referenced by nothing and carrying no tag: minted before the claim
|
|
179
|
+
component existed, so outside the rule entirely and never collected. */
|
|
180
|
+
unreferencedUntagged: array<object>,
|
|
181
|
+
/** An event references it and the object is not there. Not this mechanism's
|
|
182
|
+
doing — expiry only ever removes untagged-by-nobody objects — but a broken
|
|
183
|
+
image is worth surfacing while everything is being counted anyway. */
|
|
184
|
+
danglingRefs: array<string>,
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let reconcileStore = async (
|
|
188
|
+
~store: string,
|
|
189
|
+
~bucket: string,
|
|
190
|
+
~prefix: string,
|
|
191
|
+
~tables: array<string>,
|
|
192
|
+
): report => {
|
|
193
|
+
let refs = Set.make()
|
|
194
|
+
let eventRows = ref(0)
|
|
195
|
+
await tables->Array.reduce(Promise.resolve(), (acc, table) =>
|
|
196
|
+
acc->Promise.then(async _ => {
|
|
197
|
+
let rows = await refsInTable(~table, ~prefix, refs)
|
|
198
|
+
eventRows := eventRows.contents + rows
|
|
199
|
+
})
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
let objects = await listObjects(~bucket, ~prefix)
|
|
203
|
+
let present = Set.make()
|
|
204
|
+
objects->Array.forEach(o => present->Set.add(o.ref))
|
|
205
|
+
|
|
206
|
+
{
|
|
207
|
+
store,
|
|
208
|
+
bucket,
|
|
209
|
+
prefix,
|
|
210
|
+
eventRows: eventRows.contents,
|
|
211
|
+
referencedButPending: objects->Array.filter(o => o.pending && refs->Set.has(o.ref)),
|
|
212
|
+
unreferencedPending: objects->Array.filter(o => o.pending && !(refs->Set.has(o.ref))),
|
|
213
|
+
unreferencedUntagged: objects->Array.filter(o => !o.pending && !(refs->Set.has(o.ref))),
|
|
214
|
+
danglingRefs: refs->Set.toArray->Array.filter(r => !(present->Set.has(r))),
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let sample = (objects: array<object>, ~limit: int=10): array<object> =>
|
|
219
|
+
objects->Array.slice(~start=0, ~end=limit)
|
|
220
|
+
|
|
221
|
+
/** Print one store's report. Returns `true` when the store is safe to enable
|
|
222
|
+
expiry on — that is, when nothing referenced is still tagged. */
|
|
223
|
+
let print = (r: report): bool => {
|
|
224
|
+
Console.log("")
|
|
225
|
+
Console.log(` ${r.store} ${r.bucket}/${r.prefix}/`)
|
|
226
|
+
Console.log(` event rows scanned: ${r.eventRows->Int.toString}`)
|
|
227
|
+
let line = (label, n) => Console.log(` ${n->Int.toString->String.padStart(8, " ")} ${label}`)
|
|
228
|
+
line("referenced, still tagged pending ← must be 0", r.referencedButPending->Array.length)
|
|
229
|
+
line("unreferenced, tagged pending ← what expiry would delete", r.unreferencedPending->Array.length)
|
|
230
|
+
line("unreferenced, untagged ← outside the rule", r.unreferencedUntagged->Array.length)
|
|
231
|
+
line("referenced but missing ← dangling refs", r.danglingRefs->Array.length)
|
|
232
|
+
|
|
233
|
+
// Every object untagged reads as "the claimer has caught up with everything",
|
|
234
|
+
// and that is usually what it is. But it is also exactly what a drifted tag key
|
|
235
|
+
// looks like, and the two are indistinguishable from the counts alone — so say
|
|
236
|
+
// so rather than let a store pass on evidence that might mean nothing.
|
|
237
|
+
let objectCount =
|
|
238
|
+
r.referencedButPending->Array.length +
|
|
239
|
+
r.unreferencedPending->Array.length +
|
|
240
|
+
r.unreferencedUntagged->Array.length
|
|
241
|
+
if objectCount > 0 && r.referencedButPending->Array.length == 0 && r.unreferencedPending->Array.length == 0 {
|
|
242
|
+
Console.log("")
|
|
243
|
+
Console.log(
|
|
244
|
+
` Note: none of the ${objectCount->Int.toString} objects carry "${pendingTagKey}". That is ` ++
|
|
245
|
+
"what a fully-claimed store looks like — and also what it looks like if the mint side " ++
|
|
246
|
+
"never wrote the tag (a store whose objects all predate it) or if this tool's tag key has " ++
|
|
247
|
+
"drifted from the deploy's.",
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if r.referencedButPending->Array.length > 0 {
|
|
252
|
+
Console.log("")
|
|
253
|
+
Console.log(" Referenced objects that are still tagged pending:")
|
|
254
|
+
sample(r.referencedButPending)->Array.forEach(o => Console.log(` ${o.ref}`))
|
|
255
|
+
if r.referencedButPending->Array.length > 10 {
|
|
256
|
+
Console.log(` … and ${(r.referencedButPending->Array.length - 10)->Int.toString} more`)
|
|
257
|
+
}
|
|
258
|
+
Console.log("")
|
|
259
|
+
Console.log(
|
|
260
|
+
" Do NOT enable this store's expiry rule. Either the claim component is behind or " ++
|
|
261
|
+
"stopped (check its IteratorAge alarm), or a field holding these refs is missing its " ++
|
|
262
|
+
"`@storageRef` annotation — in which case the claimer never sees it and never will.",
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
r.referencedButPending->Array.length == 0
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Reconcile every declared object store of a deployed stack.
|
|
270
|
+
*
|
|
271
|
+
* Reads the platform project's `objectStores` output for the store list, and
|
|
272
|
+
* discovers each plugin project's DynamoDB tables by the framework's
|
|
273
|
+
* `reventless:platform` + `reventless:environment` tags — the same scoping the
|
|
274
|
+
* reset uses, for the same reason: a stack name alone is shared across projects.
|
|
275
|
+
*
|
|
276
|
+
* `targets` are the deployment's Pulumi projects, exactly as `Reset.run` takes
|
|
277
|
+
* them. Exits non-zero when any store has a referenced object still tagged
|
|
278
|
+
* pending, so a scheduled run of this is a usable check and not just a page of
|
|
279
|
+
* numbers.
|
|
280
|
+
*/
|
|
281
|
+
let run = (~stack=?, ~backend=?, ~targets: array<ReventlessSeedAws_Reset.target>, ()): unit => {
|
|
282
|
+
let go = async () => {
|
|
283
|
+
try {
|
|
284
|
+
if targets->Array.length == 0 {
|
|
285
|
+
throw(Seed.Failed("no targets were declared — nothing to reconcile."))
|
|
286
|
+
}
|
|
287
|
+
let backend = switch Seed.Prompt.envValue("SEED_PULUMI_BACKEND") {
|
|
288
|
+
| Some(url) => Some(url)
|
|
289
|
+
| None => backend
|
|
290
|
+
}
|
|
291
|
+
let platformTarget = switch targets->Array.find(t => t.group == Platform) {
|
|
292
|
+
| Some(t) => t
|
|
293
|
+
| None =>
|
|
294
|
+
throw(
|
|
295
|
+
Seed.Failed(
|
|
296
|
+
"no `platform` target is declared — declared object stores live in the platform " ++
|
|
297
|
+
"project's stack output and cannot be resolved without it.",
|
|
298
|
+
),
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
let stack = await ReventlessSeedAws.resolveStack(
|
|
302
|
+
~projectDir=platformTarget.projectDir,
|
|
303
|
+
~backend,
|
|
304
|
+
~stack,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
let output = ReventlessSeedAws.stackOutputs(
|
|
308
|
+
~projectDir=platformTarget.projectDir,
|
|
309
|
+
~backend,
|
|
310
|
+
stack,
|
|
311
|
+
)
|
|
312
|
+
let stores = switch ReventlessSeedAws_Reset.parseObjectStores(
|
|
313
|
+
output->ReventlessSeedAws_Reset.field("objectStores"),
|
|
314
|
+
) {
|
|
315
|
+
| Ok(stores) => stores
|
|
316
|
+
| Error(message) => throw(Seed.Failed(message))
|
|
317
|
+
}
|
|
318
|
+
if stores->Array.length == 0 {
|
|
319
|
+
Seed.Runner.heading(`Reconcile: stack "${stack}" declares no object stores.`)
|
|
320
|
+
} else {
|
|
321
|
+
let region = switch Seed.Prompt.envValue("AWS_REGION") {
|
|
322
|
+
| Some(r) if r != "" => r
|
|
323
|
+
| _ =>
|
|
324
|
+
throw(Seed.Failed("could not resolve the AWS region — set AWS_REGION."))
|
|
325
|
+
}
|
|
326
|
+
Seed.Runner.heading(`Reconcile: stack "${stack}" in ${region}`)
|
|
327
|
+
|
|
328
|
+
// A store's objects are referenced by the events of the plugin that
|
|
329
|
+
// declared it — but a qualified `@storageRef("Other.store")` means
|
|
330
|
+
// another plugin's events can carry them too, so every target's tables
|
|
331
|
+
// are scanned for every store rather than only the owner's.
|
|
332
|
+
let tables = []
|
|
333
|
+
await targets->Array.reduce(Promise.resolve(), (acc, target) =>
|
|
334
|
+
acc->Promise.then(async _ => {
|
|
335
|
+
let platform = ReventlessSeedAws_Reset.projectName(~projectDir=target.projectDir)
|
|
336
|
+
let (found, _buckets) = await ReventlessSeedAws_Reset.discover(
|
|
337
|
+
~region,
|
|
338
|
+
~stack,
|
|
339
|
+
~platform,
|
|
340
|
+
)
|
|
341
|
+
found->Array.forEach(t => tables->Array.push(t))
|
|
342
|
+
})
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
let allClear = ref(true)
|
|
346
|
+
await stores->Array.reduce(Promise.resolve(), (acc, s) =>
|
|
347
|
+
acc->Promise.then(async _ => {
|
|
348
|
+
let report = await reconcileStore(
|
|
349
|
+
~store=s.qualified,
|
|
350
|
+
~bucket=s.bucketName,
|
|
351
|
+
~prefix=s.keyPrefix,
|
|
352
|
+
~tables,
|
|
353
|
+
)
|
|
354
|
+
if !print(report) {
|
|
355
|
+
allClear := false
|
|
356
|
+
}
|
|
357
|
+
})
|
|
358
|
+
)
|
|
359
|
+
Console.log("")
|
|
360
|
+
if allClear.contents {
|
|
361
|
+
Console.log(
|
|
362
|
+
"Every referenced object has been claimed. Enabling a store's expiry rule " ++
|
|
363
|
+
"(`pendingUploadExpiryDays: \"<plugin>.<store>=<days>\"`) would delete only " ++
|
|
364
|
+
"objects nothing references.",
|
|
365
|
+
)
|
|
366
|
+
} else {
|
|
367
|
+
throw(
|
|
368
|
+
Seed.Failed(
|
|
369
|
+
"at least one referenced object is still tagged pending — see above. No store's " ++
|
|
370
|
+
"expiry rule should be enabled while this is true.",
|
|
371
|
+
),
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
NodeProcess.exit(0)
|
|
376
|
+
} catch {
|
|
377
|
+
| Seed.Failed(message) =>
|
|
378
|
+
Console.error("")
|
|
379
|
+
Console.error(`Reconcile failed — ${message}`)
|
|
380
|
+
NodeProcess.exit(1)
|
|
381
|
+
| exn =>
|
|
382
|
+
Console.error("")
|
|
383
|
+
Console.error("Reconcile failed with an unexpected error:")
|
|
384
|
+
Console.error(exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("unknown"))
|
|
385
|
+
NodeProcess.exit(1)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
go()->ignore
|
|
389
|
+
}
|