unoverse 0.1.102 → 0.1.104

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.
@@ -11,6 +11,9 @@ terraform {
11
11
  aws = { source = "hashicorp/aws", version = ">= 5.83, < 6.0" }
12
12
  random = { source = "hashicorp/random", version = "~> 3.6" }
13
13
  archive = { source = "hashicorp/archive", version = "~> 2.4" }
14
+ # For the IAM propagation wait below. IAM is eventually consistent and Lambda is the
15
+ # one resource here that reliably outruns it.
16
+ time = { source = "hashicorp/time", version = "~> 0.11" }
14
17
  }
15
18
  }
16
19
 
@@ -369,7 +372,19 @@ resource "aws_iam_role_policy_attachment" "pretoken_logs" {
369
372
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
370
373
  }
371
374
 
375
+ # IAM IS EVENTUALLY CONSISTENT, AND LAMBDA OUTRUNS IT. The role is created, Lambda is
376
+ # created two seconds later, and AWS answers "The role defined for the function cannot be
377
+ # assumed by Lambda" — the role is correct and simply not visible yet. Terraform's
378
+ # dependency graph is satisfied because the role exists; AWS's own propagation is not.
379
+ #
380
+ # Ten seconds is the usual advice and it costs ten seconds on a first apply only.
381
+ resource "time_sleep" "iam_propagation" {
382
+ depends_on = [aws_iam_role.pretoken, aws_iam_role_policy_attachment.pretoken_logs]
383
+ create_duration = "10s"
384
+ }
385
+
372
386
  resource "aws_lambda_function" "pretoken" {
387
+ depends_on = [time_sleep.iam_propagation]
373
388
  function_name = "${var.name}-pretoken"
374
389
  role = aws_iam_role.pretoken.arn
375
390
  runtime = "nodejs20.x"
@@ -563,7 +563,7 @@ _ground_apply() {
563
563
  echo ""
564
564
  info "Building. Managed databases take a few minutes on the provider's side"
565
565
  echo ""
566
- terraform -chdir="$dir" apply -input=false "$planfile" || { fail "The build did not finish. Re-run: unoverse deploy"; rm -rf "$tmp"; return 1; }
566
+ terraform -chdir="$dir" apply -input=false "$planfile" || { fail "The build did not finish. Nothing already built is lost — re-run: unoverse deploy $cloud"; rm -rf "$tmp"; return 1; }
567
567
  rm -rf "$tmp"
568
568
  return 0
569
569
  }
@@ -141,26 +141,43 @@ process.stdin.on("end", () => {
141
141
 
142
142
  const groups = { create: [], update: [], delete: [], replace: [] };
143
143
  const supporting = { create: 0, update: 0, delete: 0, replace: 0 };
144
+ // LOSING SOMETHING IS NEVER ANONYMOUS. Counting the plumbing is right when things are
145
+ // being built — nobody needs eleven IAM attachments enumerated. It is wrong the moment
146
+ // anything is destroyed or replaced: "plus 1 supporting resource" was a security group
147
+ // about to be deleted, and the operator hit Ctrl-C when Terraform printed "Destroying"
148
+ // for something the summary had not named. Anything being taken away is listed.
149
+ const named = { delete: [], replace: [] };
144
150
 
145
151
  for (const r of plan.resource_changes ?? []) {
146
152
  const act = action(r.change ?? {});
147
153
  if (!act) continue;
148
154
  const d = describe(r);
149
155
  if (d) groups[act].push(d);
150
- else supporting[act]++;
156
+ else if (act === "delete" || act === "replace") {
157
+ // Turn the terraform type into something readable: aws_security_group → security group
158
+ const label = r.type.replace(/^(aws|digitalocean|random|time|archive)_/, "").replace(/_/g, " ");
159
+ const v = r.change?.before ?? r.change?.after ?? {};
160
+ named[act].push({ what: label.charAt(0).toUpperCase() + label.slice(1), detail: v.name ?? v.id ?? "" });
161
+ } else supporting[act]++;
151
162
  }
152
163
 
153
- const total = Object.values(groups).flat().length + Object.values(supporting).reduce((a, b) => a + b, 0);
164
+ const total = Object.values(groups).flat().length
165
+ + Object.values(supporting).reduce((a, b) => a + b, 0)
166
+ + named.delete.length + named.replace.length;
154
167
  const out = [];
155
168
  if (total === 0) {
156
169
  console.log(`\n ${green("●")} Your infrastructure already matches. Nothing to change.\n`);
157
170
  return;
158
171
  }
159
172
 
160
- const width = Math.max(...Object.values(groups).flat().map((d) => d.what.length), 0) + 2;
173
+ const width = Math.max(
174
+ ...Object.values(groups).flat().map((d) => d.what.length),
175
+ ...named.delete.map((d) => d.what.length),
176
+ ...named.replace.map((d) => d.what.length),
177
+ 0) + 2;
161
178
 
162
179
  for (const act of ["create", "update", "replace", "delete"]) {
163
- const list = groups[act];
180
+ const list = [...groups[act], ...(named[act] ?? [])];
164
181
  const extra = supporting[act];
165
182
  if (!list.length && !extra) continue;
166
183
  const colour = act === "delete" || act === "replace" ? red : act === "update" ? yellow : green;
@@ -194,6 +211,6 @@ process.stdin.on("end", () => {
194
211
  // something is not. `process.exitCode` rather than `process.exit`, which can cut off
195
212
  // the output above while it is still draining down a pipe.
196
213
  const destructive =
197
- groups.delete.length + groups.replace.length + supporting.delete + supporting.replace;
214
+ groups.delete.length + groups.replace.length + named.delete.length + named.replace.length;
198
215
  if (destructive) process.exitCode = 3;
199
216
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unoverse",
3
- "version": "0.1.102",
3
+ "version": "0.1.104",
4
4
  "description": "The Unoverse front door — create a Studio project, a universe, or a client app, and launch Studio.",
5
5
  "license": "SEE LICENSE IN README.md",
6
6
  "type": "module",