unoverse 0.1.76 → 0.1.77

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.
@@ -14,8 +14,41 @@ terraform {
14
14
  }
15
15
  }
16
16
 
17
+ # ── One universe, one group ───────────────────────────────────────────────────
18
+ #
19
+ # AWS has no projects. Its equivalent is TAGS, and `default_tags` is the only honest way
20
+ # to apply them: it stamps every resource this configuration creates, so nothing is
21
+ # forgotten the day someone adds a resource and does not think about tagging. The console
22
+ # view comes from the Resource Group below, and the same tag drives cost allocation, which
23
+ # a DigitalOcean project cannot do.
24
+ #
25
+ # It touches ONLY what this stack creates. An adopted database keeps its own tags, in
26
+ # keeping with the rule that a universe never takes ownership of what it borrowed
27
+ # (INFRASTRUCTURE.md, "Borrowed resources are never owned").
17
28
  provider "aws" {
18
29
  region = var.region
30
+
31
+ default_tags {
32
+ tags = {
33
+ Universe = var.name
34
+ ManagedBy = "unoverse"
35
+ Size = var.size
36
+ }
37
+ }
38
+ }
39
+
40
+ # The console's answer to "what does this universe own". A tag query rather than a
41
+ # membership list, so it stays correct as the ground grows without anyone maintaining it.
42
+ resource "aws_resourcegroups_group" "universe" {
43
+ name = var.name
44
+ description = "unoverse universe: ${var.name}"
45
+
46
+ resource_query {
47
+ query = jsonencode({
48
+ ResourceTypeFilters = ["AWS::AllSupported"]
49
+ TagFilters = [{ Key = "Universe", Values = [var.name] }]
50
+ })
51
+ }
19
52
  }
20
53
 
21
54
  # ── Network: default VPC + two security groups ─────────────────────────────────
@@ -20,16 +20,23 @@
20
20
  # trusted-sources list, so pointing it at a borrowed cluster deletes the operator's own
21
21
  # IP, their other droplets and their apps. Read the rules, add ours if missing, write
22
22
  # them back. Nothing else moves, and running it twice changes nothing.
23
- _grant_droplet_to_adopted_db() {
23
+ # Add or remove THIS universe's droplet in an adopted cluster's trusted sources.
24
+ # _adopted_db_access <cloud> grant|revoke [droplet_id]
25
+ # Symmetric on purpose. The grant existed and nothing revoked it, so a teardown left a
26
+ # stale rule behind — and worse, terraform's own destroy of an authoritative firewall
27
+ # resource PUT an EMPTY list and locked the operator out of a database it had borrowed.
28
+ # Both directions now touch exactly one rule and never the list.
29
+ _adopted_db_access() {
24
30
  local cloud="$1" dir="$ROOT/infra/$cloud" cluster droplet_id
31
+ local mode="$2"
25
32
  cluster=$(grep -E '^existing_pg_cluster_name[[:space:]]*=' "$dir/terraform.tfvars" 2>/dev/null | sed -E 's/.*"([^"]+)".*/\1/')
26
33
  [ -n "$cluster" ] || return 0
27
34
  [ -n "${DIGITALOCEAN_TOKEN:-}" ] || return 0
28
- droplet_id=$(terraform -chdir="$dir" state show digitalocean_droplet.app 2>/dev/null | awk -F'"' '/^ *id *=/{print $2; exit}')
35
+ droplet_id="${3:-$(terraform -chdir="$dir" state show digitalocean_droplet.app 2>/dev/null | awk -F'"' '/^ *id *=/{print $2; exit}')}"
29
36
  [ -n "$droplet_id" ] || return 0
30
37
 
31
- node - "$cluster" "$droplet_id" <<'NODE'
32
- const [cluster, droplet] = process.argv.slice(2);
38
+ node - "$cluster" "$droplet_id" "$mode" <<'NODE'
39
+ const [cluster, droplet, mode] = process.argv.slice(2);
33
40
  const T = process.env.DIGITALOCEAN_TOKEN;
34
41
  const H = { Authorization: `Bearer ${T}`, "Content-Type": "application/json" };
35
42
  const api = (p, o = {}) => fetch(`https://api.digitalocean.com/v2${p}`, { headers: H, ...o });
@@ -38,13 +45,21 @@ const api = (p, o = {}) => fetch(`https://api.digitalocean.com/v2${p}`, { header
38
45
  const db = (list.databases || []).find((d) => d.name === cluster);
39
46
  if (!db) return;
40
47
  const fw = await (await api(`/databases/${db.id}/firewall`)).json();
41
- const rules = (fw.rules || []).map((r) => ({ type: r.type, value: r.value }));
42
- if (rules.some((r) => r.type === "droplet" && String(r.value) === String(droplet))) return;
43
- rules.push({ type: "droplet", value: String(droplet) });
48
+ let rules = (fw.rules || []).map((r) => ({ type: r.type, value: r.value }));
49
+ const mine = (r) => r.type === "droplet" && String(r.value) === String(droplet);
50
+ const has = rules.some(mine);
51
+ if (mode === "revoke") {
52
+ if (!has) return;
53
+ rules = rules.filter((r) => !mine(r)); // ours only; every other rule survives
54
+ } else {
55
+ if (has) return;
56
+ rules.push({ type: "droplet", value: String(droplet) });
57
+ }
44
58
  const res = await api(`/databases/${db.id}/firewall`, { method: "PUT", body: JSON.stringify({ rules }) });
59
+ const what = mode === "revoke" ? "no longer reaches" : "may reach";
45
60
  console.log(res.ok
46
- ? ` \x1b[32m✓\x1b[0m This universe may reach ${cluster} \x1b[2m(added to its trusted sources, nothing else changed)\x1b[0m`
47
- : ` \x1b[33m!\x1b[0m Could not update ${cluster}'s trusted sources — add droplet ${droplet} by hand`);
61
+ ? ` \x1b[32m✓\x1b[0m This universe ${what} ${cluster} \x1b[2m(one rule changed, ${rules.length} left in place)\x1b[0m`
62
+ : ` \x1b[33m!\x1b[0m Could not update ${cluster}'s trusted sources — adjust droplet ${droplet} by hand`);
48
63
  })().catch(() => {});
49
64
  NODE
50
65
  }
@@ -311,7 +326,7 @@ cmd_deploy() {
311
326
  exit 1
312
327
  fi
313
328
 
314
- _grant_droplet_to_adopted_db "$cloud"
329
+ _adopted_db_access "$cloud" grant
315
330
 
316
331
 
317
332
  # Read deploy target from .env.production
@@ -91,6 +91,14 @@ cmd_destroy() {
91
91
  return 1
92
92
  fi
93
93
 
94
+ # Take our rule out of the borrowed cluster BEFORE the state goes, while the droplet id
95
+ # is still readable. One rule, ours, never the list.
96
+ if [ -n "$kept" ]; then
97
+ local dropped
98
+ dropped=$(terraform -chdir="$dir" state show digitalocean_droplet.app 2>/dev/null | awk -F'"' '/^ *id *=/{print $2; exit}')
99
+ [ -n "$dropped" ] && _adopted_db_access "$cloud" revoke "$dropped"
100
+ fi
101
+
94
102
  echo ""
95
103
  info "Taking it down..."
96
104
  echo ""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unoverse",
3
- "version": "0.1.76",
3
+ "version": "0.1.77",
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",