unoverse 0.1.88 → 0.1.90
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/operator/lib/deploy.sh +74 -20
- package/operator/lib/ground.sh +27 -0
- package/operator/lib/tfsummary.mjs +15 -0
- package/package.json +1 -1
package/operator/lib/deploy.sh
CHANGED
|
@@ -260,28 +260,68 @@ _ensure_ground_config() {
|
|
|
260
260
|
fi
|
|
261
261
|
fi
|
|
262
262
|
|
|
263
|
-
|
|
264
|
-
#
|
|
265
|
-
#
|
|
266
|
-
#
|
|
263
|
+
# PRODUCTION'S DATABASE IS NOT THE ONE IN .env, AND THE QUESTION MUST SAY SO.
|
|
264
|
+
#
|
|
265
|
+
# `.env` holds the DEVELOPMENT database — what `npm run dev` talks to on the laptop. The
|
|
266
|
+
# deployed universe gets its own, so a developer cannot break production by experimenting
|
|
267
|
+
# locally. That separation is right and is the default.
|
|
268
|
+
#
|
|
269
|
+
# What was wrong was the words. This asked "You already have a database — use it?", where
|
|
270
|
+
# "it" meant the CLUSTER and "use" meant "create a new database inside it". A developer
|
|
271
|
+
# who had typed a DATABASE_URL during setup read that as "use the database I gave you",
|
|
272
|
+
# answered yes, and got an empty one — then could not find their 21 workflows and
|
|
273
|
+
# reasonably concluded the deploy had lost them. Nothing was lost; the sentence was.
|
|
274
|
+
#
|
|
275
|
+
# So: name the cluster, say a NEW database goes in it, and say what happens to the one in
|
|
276
|
+
# .env. Anyone wanting production to share the development database sets byo_postgres_url
|
|
277
|
+
# deliberately, which is a different and much louder act.
|
|
267
278
|
if [ "$cloud" = "digitalocean" ] \
|
|
268
|
-
&& grep -q '^#[[:space:]]*existing_pg_cluster_name' "$tfv" 2>/dev/null \
|
|
269
279
|
&& ! grep -q '^existing_pg_cluster_name' "$tfv" 2>/dev/null \
|
|
270
280
|
&& ! grep -q '^byo_postgres_url' "$tfv" 2>/dev/null; then
|
|
271
|
-
local found
|
|
272
|
-
found=$(grep '^#[[:space:]]*existing_pg_cluster_name' "$tfv" | sed -E 's/.*"([^"]+)".*/\1/')
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
281
|
+
local found env_db db_name
|
|
282
|
+
found=$(grep '^#[[:space:]]*existing_pg_cluster_name' "$tfv" 2>/dev/null | sed -E 's/.*"([^"]+)".*/\1/')
|
|
283
|
+
env_db=$(grep -E '^DATABASE_URL=' "$ROOT/.env" 2>/dev/null | head -1 | cut -d= -f2-)
|
|
284
|
+
db_name=$(echo "$env_db" | sed -E 's|.*/([^/?]+)(\?.*)?$|\1|')
|
|
285
|
+
|
|
286
|
+
echo ""
|
|
287
|
+
echo -e " ${CYAN}${BOLD}Which database should the DEPLOYED universe use?${NC}"
|
|
288
|
+
echo ""
|
|
289
|
+
echo -e " ${DIM}Your .env is your local development database and does not change.${NC}"
|
|
290
|
+
echo ""
|
|
291
|
+
local opt_new_db="" opt_share=""
|
|
292
|
+
[ -n "$found" ] && opt_new_db="Its own new database on ${found} free"
|
|
293
|
+
[ -n "$db_name" ] && opt_share="The same one you develop against ${db_name}"
|
|
294
|
+
|
|
295
|
+
# Order is the recommendation. Its own database first: production and development stay
|
|
296
|
+
# independent, which is what almost everyone wants and what nobody regrets.
|
|
297
|
+
if [ -n "$opt_new_db" ] && [ -n "$opt_share" ]; then
|
|
298
|
+
pick_option "$opt_new_db" "$opt_share" "A new cluster of its own ~\$15/month"
|
|
299
|
+
elif [ -n "$opt_new_db" ]; then
|
|
300
|
+
pick_option "$opt_new_db" "A new cluster of its own ~\$15/month"
|
|
301
|
+
elif [ -n "$opt_share" ]; then
|
|
302
|
+
pick_option "$opt_share" "A new cluster of its own ~\$15/month"
|
|
303
|
+
else
|
|
304
|
+
PICKED=99 # nothing to reuse: the ground provisions a cluster, no question worth asking
|
|
284
305
|
fi
|
|
306
|
+
|
|
307
|
+
local choice=""
|
|
308
|
+
case "$PICKED" in
|
|
309
|
+
0) [ -n "$opt_new_db" ] && choice="own" || choice="share" ;;
|
|
310
|
+
1) [ -n "$opt_new_db" ] && [ -n "$opt_share" ] && choice="share" || choice="fresh" ;;
|
|
311
|
+
*) choice="fresh" ;;
|
|
312
|
+
esac
|
|
313
|
+
|
|
314
|
+
case "$choice" in
|
|
315
|
+
own)
|
|
316
|
+
node -e 'const fs=require("fs");const[f]=process.argv.slice(1);let s=fs.readFileSync(f,"utf8");s=s.replace(/^#\s*(existing_pg_cluster_name\s*=.*)$/m,"$1");fs.writeFileSync(f,s)' "$tfv"
|
|
317
|
+
ok "Production gets its own database on $found ${DIM}(your development data is untouched)${NC}"
|
|
318
|
+
;;
|
|
319
|
+
share)
|
|
320
|
+
node -e 'const fs=require("fs");const[f,v]=process.argv.slice(1);let s=fs.readFileSync(f,"utf8");s=s.replace(/^#?\s*byo_postgres_url\s*=.*$/m,"byo_postgres_url = "+JSON.stringify(v));fs.writeFileSync(f,s)' "$tfv" "$env_db"
|
|
321
|
+
ok "Production reads ${BOLD}$db_name${NC} ${DIM}(the same database you develop against — local changes are live)${NC}"
|
|
322
|
+
;;
|
|
323
|
+
*) ok "A new cluster will be created for production" ;;
|
|
324
|
+
esac
|
|
285
325
|
fi
|
|
286
326
|
|
|
287
327
|
_tf_fill docr_token DOCR_TOKEN "Registry access token (from your Unoverse admin)"
|
|
@@ -401,12 +441,26 @@ _ground_apply() {
|
|
|
401
441
|
|
|
402
442
|
if [ "$summary_rc" = "3" ]; then
|
|
403
443
|
local REPLY
|
|
444
|
+
# ENTER GOES AHEAD, ESC STOPS. This asked for a typed `y` and made Enter mean "stop",
|
|
445
|
+
# which is the opposite of every other prompt in this CLI — the developer who has just
|
|
446
|
+
# read the plan and decided has to break the habit of the whole tool to act on it. One
|
|
447
|
+
# meaning for Enter throughout, and the escape hatch is the key literally named for it.
|
|
404
448
|
echo -e " ${RED}This removes or rebuilds things that already exist.${NC}"
|
|
405
|
-
|
|
406
|
-
|
|
449
|
+
echo ""
|
|
450
|
+
echo -e " ${DIM}Enter to go ahead · Esc to stop${NC}"
|
|
451
|
+
printf " "
|
|
452
|
+
local key=""
|
|
453
|
+
if [ -t 0 ]; then
|
|
454
|
+
IFS= read -r -s -n 1 key
|
|
455
|
+
fi
|
|
456
|
+
# \e is Escape; anything else typed (n, q, a stray letter) also stops, because only a
|
|
457
|
+
# deliberate empty Enter should destroy something.
|
|
458
|
+
if [ -n "$key" ]; then
|
|
459
|
+
echo ""
|
|
407
460
|
info "Nothing was changed. Run ${BOLD}unoverse deploy${NC} when you are ready"
|
|
408
461
|
rm -rf "$tmp"; return 1
|
|
409
462
|
fi
|
|
463
|
+
echo ""
|
|
410
464
|
fi
|
|
411
465
|
|
|
412
466
|
echo ""
|
package/operator/lib/ground.sh
CHANGED
|
@@ -13,6 +13,31 @@ _ground_my_ip() {
|
|
|
13
13
|
curl -s --max-time 5 https://api.ipify.org 2>/dev/null || curl -s --max-time 5 https://ifconfig.me 2>/dev/null
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
# A GROUND WRITES SECRETS TO DISK, so the ignore rule ships with the ground itself.
|
|
17
|
+
#
|
|
18
|
+
# terraform.tfvars holds the database URL, the registry token and every service key; the
|
|
19
|
+
# state file holds those plus everything Terraform generated, in plain text. The platform
|
|
20
|
+
# repo has ignored them since the grounds landed. A universe scaffolded by the CLI did not,
|
|
21
|
+
# because its .gitignore comes from the starter — which only changes when the starter is
|
|
22
|
+
# re-synced and pushed, long after somebody's first `git init && git add .`.
|
|
23
|
+
#
|
|
24
|
+
# Fixing it here means the rule exists the moment the file it protects does, on every
|
|
25
|
+
# universe, without waiting for a starter release. Idempotent: it appends only what is
|
|
26
|
+
# missing, and never rewrites a line the developer put there.
|
|
27
|
+
_ground_protect_secrets() {
|
|
28
|
+
local gi="$ROOT/.gitignore" rule
|
|
29
|
+
[ -f "$gi" ] || : > "$gi"
|
|
30
|
+
local added=0
|
|
31
|
+
for rule in 'infra/**/.terraform/' 'infra/**/*.tfstate*' 'infra/**/terraform.tfvars' '.unoverse/'; do
|
|
32
|
+
grep -qxF "$rule" "$gi" 2>/dev/null && continue
|
|
33
|
+
[ "$added" = "0" ] && printf '\n# Terraform (infra/*) — state and vars carry secrets\n' >> "$gi"
|
|
34
|
+
printf '%s\n' "$rule" >> "$gi"
|
|
35
|
+
added=1
|
|
36
|
+
done
|
|
37
|
+
[ "$added" = "1" ] && ok "Added the terraform files to .gitignore ${DIM}(they hold your database URL and keys)${NC}"
|
|
38
|
+
return 0
|
|
39
|
+
}
|
|
40
|
+
|
|
16
41
|
# THE UNIVERSE HAS A NAME, so ask for one. This file used to write name = "universe-poc"
|
|
17
42
|
# as a literal, so every universe on every account carried the same placeholder — it names
|
|
18
43
|
# the droplet, the cache, the load balancer and the cloud project, and the developer first
|
|
@@ -132,6 +157,7 @@ _ground_do() {
|
|
|
132
157
|
echo ""
|
|
133
158
|
echo -e " ${DIM}Regions available to you: $regions${NC}"
|
|
134
159
|
fi
|
|
160
|
+
_ground_protect_secrets
|
|
135
161
|
_ground_identity "$regions"
|
|
136
162
|
|
|
137
163
|
cat > "$out" <<EOF
|
|
@@ -232,6 +258,7 @@ _ground_aws() {
|
|
|
232
258
|
fi
|
|
233
259
|
|
|
234
260
|
# Region already came from the AWS CLI's own configuration, so only the name is asked.
|
|
261
|
+
_ground_protect_secrets
|
|
235
262
|
_ground_identity "-"
|
|
236
263
|
|
|
237
264
|
cat > "$out" <<EOF
|
|
@@ -71,6 +71,17 @@ function describe(r) {
|
|
|
71
71
|
price: PRICE[a.size],
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
|
+
// NEVER "SUPPORTING". These were counted as plumbing, which is fair when they are being
|
|
75
|
+
// created alongside the cluster they live on — and badly wrong on a REPLACE, where a
|
|
76
|
+
// dropped and recreated database takes its contents with it. "plus 1 supporting
|
|
77
|
+
// resource (users, rules, keys)" is not something anyone reads as "your data is about
|
|
78
|
+
// to be deleted". Naming them costs one line and makes that impossible to miss.
|
|
79
|
+
case "digitalocean_database_db":
|
|
80
|
+
return { what: "Database", detail: a.name ? `${a.name} — its contents live here` : "" };
|
|
81
|
+
case "digitalocean_database_user":
|
|
82
|
+
return { what: "Database user", detail: a.name ?? "" };
|
|
83
|
+
case "digitalocean_database_connection_pool":
|
|
84
|
+
return { what: "Connection pool", detail: a.name ?? "" };
|
|
74
85
|
case "digitalocean_loadbalancer":
|
|
75
86
|
return { what: "Load balancer", detail: "the public way in", price: PRICE.lb };
|
|
76
87
|
case "digitalocean_firewall":
|
|
@@ -168,7 +179,11 @@ process.stdin.on("end", () => {
|
|
|
168
179
|
plan.planned_values?.outputs?.monthly_estimate?.value ??
|
|
169
180
|
plan.outputs?.monthly_estimate?.value;
|
|
170
181
|
const monthly = ground ?? Object.values(groups).flat().reduce((sum, d) => sum + (d.price ?? 0), 0);
|
|
182
|
+
// THE TOTAL, NOT THE PRICE OF THIS CHANGE. `monthly_estimate` is what the whole universe
|
|
183
|
+
// costs to run, and printing it under a change list read as the bill for that change:
|
|
184
|
+
// a one-line firewall update appeared to cost $135. Say which number this is.
|
|
171
185
|
if (monthly) {
|
|
186
|
+
out.push(` ${dim("Running total for the whole universe, not the cost of this change:")}`);
|
|
172
187
|
out.push(` ${bold("Roughly")} ${bold(`$${monthly}/month`)} ${dim("at your provider's current prices, billed by them, not by unoverse")}`);
|
|
173
188
|
out.push("");
|
|
174
189
|
}
|
package/package.json
CHANGED