unoverse 0.1.124 → 0.1.126

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.
@@ -18,6 +18,84 @@
18
18
  # It never touches the developer's terraform.tfvars, their .env, or their code. Only the
19
19
  # cloud resources this ground owns.
20
20
 
21
+ # ── Resources a previous run created and did not write down ───────────────────────────────
22
+ #
23
+ # `destroy` removes what STATE records. A deploy killed mid-apply leaves AWS holding
24
+ # resources terraform never recorded, and they do not merely linger: a database or a load
25
+ # balancer keeps a security group alive, so the part terraform CAN do fails on
26
+ #
27
+ # DependencyViolation: resource sg-... has a dependent object
28
+ #
29
+ # after fifteen minutes of retrying, and the teardown reports only that it did not finish.
30
+ # The operator is then left deleting a database, a Redis cluster and a load balancer by hand
31
+ # before a "one command" teardown will complete. That happened on 2026-08-05.
32
+ #
33
+ # IT IMPORTS. IT NEVER DELETES.
34
+ #
35
+ # Adopting an orphan into state is a state write and touches no cloud API. The deletion is
36
+ # still terraform's, from the plan the operator sees and confirms by name. That matters twice
37
+ # over: terraform already knows the order to take this stack apart (load balancer before its
38
+ # security group, database before the group holding its interface), and a second deletion
39
+ # engine written in bash would not, and would drift from main.tf the day a resource was
40
+ # added.
41
+ #
42
+ # NOTHING WE DID NOT CREATE, enforced four ways rather than trusted:
43
+ #
44
+ # 1. TAGGED AS OURS. `_cloud_orphans` lists only what carries `Universe=<name>`, and
45
+ # `default_tags` stamps that on resources THIS configuration creates. A borrowed
46
+ # database keeps its own tags and can never appear here (main.tf, "Borrowed resources
47
+ # are never owned").
48
+ # 2. A KNOWN ADDRESS. The ARN must map to one of the handful of resources main.tf declares.
49
+ # Anything else is left alone, whatever it is tagged.
50
+ # 3. NAMED AFTER THIS UNIVERSE. The ARN must contain the universe's own name, which is why
51
+ # `aws_instance.app` is deliberately absent below: an EC2 ARN carries only an opaque
52
+ # instance id, so it cannot be verified this way, and an unverifiable match is not one
53
+ # worth making. An untracked server is reported instead.
54
+ # 4. NOT ALREADY TRACKED. An address present in state is skipped, so this can only ever add
55
+ # what is missing, never re-point something the operator already owns.
56
+ #
57
+ # Failure is not fatal anywhere in here: the worst case is the teardown behaving exactly as
58
+ # it did before.
59
+ _adopt_orphans() {
60
+ local dir="$1" uname_="$2" orphans arn addr id adopted=0 unclaimed=""
61
+
62
+ command -v aws >/dev/null 2>&1 || return 0
63
+ orphans=$(_cloud_orphans aws "$uname_" 2>/dev/null)
64
+ [ -n "$orphans" ] && [ "$orphans" != "unknown" ] || return 0
65
+
66
+ while IFS= read -r arn; do
67
+ [ -n "$arn" ] || continue
68
+ addr=""; id="$arn"
69
+ case "$arn" in
70
+ *:rds:*:db:"$uname_"-pg) addr="aws_db_instance.postgres"; id="$uname_-pg" ;;
71
+ *:elasticache:*:replicationgroup:"$uname_"-redis) addr="aws_elasticache_replication_group.redis"; id="$uname_-redis" ;;
72
+ *:loadbalancer/app/"$uname_"-alb/*) addr="aws_lb.public" ;;
73
+ *:targetgroup/"$uname_"-app/*) addr="aws_lb_target_group.app" ;;
74
+ *:targetgroup/"$uname_"-canvas/*) addr="aws_lb_target_group.canvas[0]" ;;
75
+ *) unclaimed="$unclaimed $arn"$'\n' ; continue ;;
76
+ esac
77
+ terraform -chdir="$dir" state list 2>/dev/null | grep -qxF "$addr" && continue
78
+ if terraform -chdir="$dir" import -input=false "$addr" "$id" >/dev/null 2>&1; then
79
+ adopted=$((adopted + 1))
80
+ else
81
+ unclaimed="$unclaimed $arn"$'\n'
82
+ fi
83
+ done <<EOF
84
+ $orphans
85
+ EOF
86
+
87
+ [ "$adopted" -gt 0 ] && \
88
+ info "Adopted $adopted resource(s) an interrupted run left untracked. They are in the plan below"
89
+ # SAID OUT LOUD, never silently swept. Anything here is billing and this teardown will not
90
+ # remove it, which the operator has to know BEFORE they are told the universe is gone.
91
+ if [ -n "$unclaimed" ]; then
92
+ warn "these carry this universe's tag but are not resources this teardown can remove:"
93
+ printf '%s' "$unclaimed"
94
+ echo " Check them yourself before assuming the bill has stopped."
95
+ fi
96
+ return 0
97
+ }
98
+
21
99
  cmd_destroy() {
22
100
  # NAME THE GROUND. This took the first configured one, and digitalocean is first in the
23
101
  # list — so an operator with both grounds who typed `unoverse destroy` meaning AWS would
@@ -46,10 +124,20 @@ cmd_destroy() {
46
124
  echo -e " ${RED}${BOLD}⬡ Taking down your $pretty universe${NC}"
47
125
  echo ""
48
126
 
49
- local tmp planfile rc
127
+ local tmp planfile rc name
50
128
  tmp=$(mktemp -d); planfile="$tmp/plan"
51
129
 
52
- # APPLY THE CONFIGURATION FIRST, because destroy does not.
130
+ # The universe's name, read once. It is the tag every resource carries, so it is what
131
+ # decides which untracked resources are this universe's (_adopt_orphans) as well as the
132
+ # word the operator types to confirm.
133
+ name=$(grep -E '^name[[:space:]]*=' "$dir/terraform.tfvars" 2>/dev/null | sed -E 's/.*"([^"]+)".*/\1/')
134
+ name="${name:-universe}"
135
+
136
+ # BEFORE THE PLAN, so what an interrupted run left behind is in the list the operator
137
+ # confirms, rather than something they discover by hand an hour later.
138
+ [ "$cloud" = "aws" ] && _adopt_orphans "$dir" "$name"
139
+
140
+ # APPLY THE CONFIGURATION TO THE DATABASE FIRST, because destroy does not.
53
141
  #
54
142
  # `terraform destroy` deletes a resource as the last apply left it, so an attribute that
55
143
  # matters only AT DELETE — RDS's `skip_final_snapshot` — comes from STATE, not from
@@ -57,10 +145,23 @@ cmd_destroy() {
57
145
  # already applied. Applying the config to the database writes those settings into state
58
146
  # and calls no API; the resource is deleted seconds later, so nothing else here matters.
59
147
  #
60
- # It is the standard terraform answer (apply, then destroy) rather than a special case,
61
- # and it is best-effort: if it fails, the destroy below reports the real problem.
62
- [ "$cloud" = "aws" ] && terraform -chdir="$dir" apply -input=false -auto-approve \
63
- -target=aws_db_instance.postgres >"$tmp/sync.log" 2>&1
148
+ # ONLY IF IT IS ALREADY IN STATE. Without that test this is not an update, it is a CREATE:
149
+ # `apply -target` on a resource the configuration declares and the state does not have
150
+ # builds it. So a teardown of a universe whose database had already gone — deleted by
151
+ # hand, or removed from state — silently provisioned a NEW one, with -auto-approve, on the
152
+ # way to being told to destroy everything. In state means the only possible change is to
153
+ # the resource's own attributes.
154
+ #
155
+ # ANNOUNCED, because it runs before the first progress line and terraform can sit on an
156
+ # RDS refresh for a while. Sending it to a log with nothing on screen made `unoverse
157
+ # destroy` look frozen at the banner.
158
+ if [ "$cloud" = "aws" ] \
159
+ && terraform -chdir="$dir" state list 2>/dev/null | grep -qx "aws_db_instance.postgres"; then
160
+ info "Checking the database's delete settings..."
161
+ terraform -chdir="$dir" apply -input=false -auto-approve \
162
+ -target=aws_db_instance.postgres >"$tmp/sync.log" 2>&1 \
163
+ || warn "could not update them; the teardown may stop on the database"
164
+ fi
64
165
 
65
166
  info "Working out what exists..."
66
167
  terraform -chdir="$dir" plan -destroy -input=false -detailed-exitcode -out="$planfile" >"$tmp/log" 2>&1
@@ -85,10 +186,6 @@ cmd_destroy() {
85
186
  info "Staying: ${BOLD}your own database${NC} ${DIM}(byo_postgres_url — this stack never owned it)${NC}"
86
187
  fi
87
188
 
88
- local name
89
- name=$(grep -E '^name[[:space:]]*=' "$dir/terraform.tfvars" 2>/dev/null | sed -E 's/.*"([^"]+)".*/\1/')
90
- name="${name:-universe}"
91
-
92
189
  echo ""
93
190
  # Say what is actually lost. With a reused cluster the DATABASE survives, and claiming
94
191
  # otherwise makes the warning untrue in the commonest case — which teaches people to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unoverse",
3
- "version": "0.1.124",
3
+ "version": "0.1.126",
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",