unoverse 0.1.99 → 0.1.100

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.
@@ -1,4 +1,56 @@
1
1
  #!/usr/bin/env bash
2
+
3
+ # Resources this universe is BILLED FOR that terraform does not know it owns.
4
+ # _cloud_orphans <ground> <universe-name>
5
+ # Echoes nothing when everything is tracked, "unknown" when the provider could not be
6
+ # reached (never a failure — an offline laptop is not an orphaned resource), or one line
7
+ # per untracked resource.
8
+ #
9
+ # It works because every resource is labelled with the universe it belongs to: AWS through
10
+ # default_tags (Universe=<name>), DigitalOcean through project membership. Without that
11
+ # label there is no question to ask, which is why the Canvas load balancer landing in the
12
+ # wrong DO project was worth fixing rather than tidying.
13
+ _cloud_orphans() {
14
+ local g="$1" uname_="$2" dir="$ROOT/infra/$1"
15
+ [ -n "$uname_" ] || { echo unknown; return 0; }
16
+
17
+ local state
18
+ state=$(terraform -chdir="$dir" show -json 2>/dev/null) || { echo unknown; return 0; }
19
+ [ -n "$state" ] || { echo unknown; return 0; }
20
+
21
+ local live
22
+ if [ "$g" = "aws" ]; then
23
+ command -v aws >/dev/null 2>&1 || { echo unknown; return 0; }
24
+ live=$(aws resourcegroupstaggingapi get-resources \
25
+ --tag-filters "Key=Universe,Values=$uname_" \
26
+ --query 'ResourceTagMappingList[].ResourceARN' --output text 2>/dev/null | tr '\t' '\n')
27
+ else
28
+ [ -n "${DIGITALOCEAN_TOKEN:-}" ] || { echo unknown; return 0; }
29
+ local pid
30
+ pid=$(doctl projects list --format ID,Name --no-header 2>/dev/null | awk -v n="$uname_" '$2==n{print $1}')
31
+ [ -n "$pid" ] || { echo unknown; return 0; }
32
+ live=$(doctl projects resources list "$pid" --format URN --no-header 2>/dev/null)
33
+ fi
34
+ [ -n "$live" ] || { echo ""; return 0; }
35
+
36
+ printf '%s' "$state" | node -e '
37
+ let s=""; process.stdin.on("data",d=>s+=d).on("end",()=>{
38
+ const live=(process.argv[1]||"").split("\n").map(x=>x.trim()).filter(Boolean);
39
+ let ids=new Set();
40
+ try {
41
+ const st=JSON.parse(s);
42
+ const walk=(m)=>{ (m.resources||[]).forEach(r=>{ if(!r.values) return;
43
+ ["id","arn","urn"].forEach(k=>r.values[k]&&ids.add(String(r.values[k]))); });
44
+ (m.child_modules||[]).forEach(walk); };
45
+ if (st.values && st.values.root_module) walk(st.values.root_module);
46
+ } catch { process.exit(0); } // unreadable state is not an orphan claim
47
+ const known=[...ids];
48
+ const orphans=live.filter(a=>!ids.has(a) && !known.some(i=>i && a.includes(i)));
49
+ orphans.forEach(o=>console.log(o));
50
+ });
51
+ ' "$live"
52
+ }
53
+
2
54
  # unoverse check
3
55
 
4
56
  cmd_check() {
@@ -86,6 +138,36 @@ cmd_check() {
86
138
  fail "Canvas ${DIM}http://localhost:3001 → $canvas_code${NC}"
87
139
  fi
88
140
 
141
+ # 6. NOTHING BILLING THAT TERRAFORM HAS LOST SIGHT OF.
142
+ #
143
+ # Terraform records what it creates as it creates it, so an interrupted apply does not
144
+ # duplicate anything — the next run continues from state. Two cases break that, and both
145
+ # cost money silently: a crash between the API call and the state write, and a lost or
146
+ # deleted state file, which orphans everything at once and makes it invisible to destroy.
147
+ #
148
+ # This is answerable because every resource is labelled with the universe it belongs to:
149
+ # AWS through default_tags, DigitalOcean through project membership. Ask the provider
150
+ # what it thinks is ours, ask terraform what it knows it made, and compare. A number on
151
+ # the health check is the difference between trusting that and verifying it.
152
+ local _g
153
+ for _g in aws digitalocean; do
154
+ [ -f "$ROOT/infra/$_g/terraform.tfvars" ] || continue
155
+ [ -d "$ROOT/infra/$_g/.terraform" ] || continue
156
+ total=$((total + 1))
157
+ local uname_ orphans
158
+ uname_=$(grep -E '^name[[:space:]]*=' "$ROOT/infra/$_g/terraform.tfvars" 2>/dev/null | sed -E 's/.*"([^"]+)".*/\1/')
159
+ orphans=$(_cloud_orphans "$_g" "$uname_")
160
+ case "$orphans" in
161
+ "") ok "Cloud resources ${DIM}($_g — every resource is tracked)${NC}"; pass=$((pass + 1)) ;;
162
+ unknown) ok "Cloud resources ${DIM}($_g — could not reach the provider, skipped)${NC}"; pass=$((pass + 1)) ;;
163
+ *)
164
+ fail "Cloud resources ${DIM}($_g — billing but NOT tracked by terraform)${NC}"
165
+ echo "$orphans" | while read -r o; do [ -n "$o" ] && echo -e " ${DIM}$o${NC}"; done
166
+ info " These will not be removed by ${BOLD}unoverse destroy $_g${NC}. Delete them in the console"
167
+ ;;
168
+ esac
169
+ done
170
+
89
171
  # Summary
90
172
  echo ""
91
173
  if [ "$pass" -eq "$total" ]; then
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unoverse",
3
- "version": "0.1.99",
3
+ "version": "0.1.100",
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",