unoverse 0.1.81 → 0.1.83
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/lib/urls.mjs
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* PROBED, not recited. Every address is actually requested and reported by what
|
|
10
10
|
* answered. A URL you cannot click is a hope, not an address.
|
|
11
11
|
*
|
|
12
|
-
* Deployed facts come from
|
|
13
|
-
*
|
|
12
|
+
* Deployed facts come from the ground's terraform outputs, which built the thing they
|
|
13
|
+
* describe. Local needs no config: a port is either listening or it is not.
|
|
14
14
|
*
|
|
15
15
|
* Named `urls` until 0.1.5; that spelling still works as an alias.
|
|
16
16
|
*/
|
|
@@ -36,31 +36,29 @@ function section(title, rows, subtitle) {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
import { existsSync
|
|
39
|
+
import { existsSync } from "node:fs";
|
|
40
40
|
import { join, resolve, dirname } from "node:path";
|
|
41
41
|
import { spawnSync } from "node:child_process";
|
|
42
42
|
|
|
43
43
|
import { bold, dim, cyan, green, red } from "./ui.mjs";
|
|
44
44
|
|
|
45
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* Walk up from cwd to the universe root, identified by its docker-compose.yml.
|
|
47
|
+
*
|
|
48
|
+
* It used to look for .env.production, which is no longer a file: deploy renders it from
|
|
49
|
+
* terraform into a temp file and deletes it. Every deployed fact below comes from the
|
|
50
|
+
* ground's outputs now, which is where it was always true — a rendered env file was only
|
|
51
|
+
* ever a copy, and a copy that outlived the server it described.
|
|
52
|
+
*/
|
|
46
53
|
function findUniverseRoot() {
|
|
47
54
|
for (let dir = process.cwd(); ; ) {
|
|
48
|
-
if (existsSync(join(dir, ".
|
|
55
|
+
if (existsSync(join(dir, "docker-compose.yml"))) return dir;
|
|
49
56
|
const parent = resolve(dir, "..");
|
|
50
57
|
if (parent === dir) return null;
|
|
51
58
|
dir = parent;
|
|
52
59
|
}
|
|
53
60
|
}
|
|
54
61
|
|
|
55
|
-
function parseEnv(file) {
|
|
56
|
-
const env = {};
|
|
57
|
-
for (const line of readFileSync(file, "utf8").split("\n")) {
|
|
58
|
-
const m = /^([A-Z0-9_]+)=(.*)$/.exec(line.trim());
|
|
59
|
-
if (m) env[m[1]] = m[2];
|
|
60
|
-
}
|
|
61
|
-
return env;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
62
|
/** The ground's terraform outputs, when a state is present. Best-effort. */
|
|
65
63
|
function terraformOutputs(root) {
|
|
66
64
|
for (const ground of ["infra/digitalocean", "infra/aws"]) {
|
|
@@ -93,17 +91,16 @@ export async function urls() {
|
|
|
93
91
|
// a fact, not a configuration question, so it needs no universe folder to answer.
|
|
94
92
|
const local = await Promise.all(LOCAL.map(async (c) => ({ ...c, ...(await probe(c.url)) })));
|
|
95
93
|
|
|
96
|
-
// DEPLOYED, only if this
|
|
97
|
-
//
|
|
94
|
+
// DEPLOYED, only if this universe has a ground with state. No outputs is not an error:
|
|
95
|
+
// plenty of universes only ever run locally. Every address here comes from terraform,
|
|
96
|
+
// which built the thing it is describing — the ground already resolves DOMAIN into
|
|
97
|
+
// api_url, so there is nothing left to work out on this side.
|
|
98
98
|
let deployed = [];
|
|
99
99
|
let domain = "";
|
|
100
100
|
if (root) {
|
|
101
|
-
const env = parseEnv(join(root, ".env.production"));
|
|
102
101
|
const tf = terraformOutputs(root);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
// explicit API_URL (rendered by terraform) carries the LB address.
|
|
106
|
-
const apiBase = domain ? `https://api.${domain}` : env.API_URL || tf.api_url || "";
|
|
102
|
+
const apiBase = tf.api_url || "";
|
|
103
|
+
domain = /^https:\/\/api\.(.+)$/.exec(apiBase)?.[1] || "";
|
|
107
104
|
const candidates = [];
|
|
108
105
|
if (apiBase) {
|
|
109
106
|
candidates.push({ label: "API", url: apiBase, note: "MCP at /mcp" });
|
|
@@ -111,7 +108,7 @@ export async function urls() {
|
|
|
111
108
|
}
|
|
112
109
|
const canvas = tf.canvas_url && tf.canvas_url.startsWith("http") ? tf.canvas_url : null;
|
|
113
110
|
if (canvas) candidates.push({ label: "Canvas", url: canvas, note: "add to IdP origins" });
|
|
114
|
-
if (
|
|
111
|
+
if (tf.deploy_host) candidates.push({ label: "Logs", url: `http://${tf.deploy_host}:8080`, note: "admin IP only" });
|
|
115
112
|
if (tf.cognito_hosted_ui) candidates.push({ label: "Login", url: tf.cognito_hosted_ui });
|
|
116
113
|
deployed = await Promise.all(candidates.map(async (c) => ({ ...c, ...(await probe(c.url)) })));
|
|
117
114
|
}
|
|
@@ -17,15 +17,21 @@
|
|
|
17
17
|
become: yes
|
|
18
18
|
|
|
19
19
|
tasks:
|
|
20
|
+
# REGISTER NAMES MUST NOT COLLIDE WITH -e VARIABLES. This registered into `env_file`,
|
|
21
|
+
# which is also the name deploy passes on the command line as the path to the rendered
|
|
22
|
+
# settings. Extra vars outrank registered ones in Ansible's precedence, so the register
|
|
23
|
+
# never took: the variable stayed a string and the very next line asked a string for
|
|
24
|
+
# `.stat`, failing with "object of type 'str' has no attribute 'stat'" — an error that
|
|
25
|
+
# says nothing about the actual cause.
|
|
20
26
|
- name: "[1/5] Verify .env exists"
|
|
21
27
|
stat:
|
|
22
28
|
path: /opt/gravity/.env
|
|
23
|
-
register:
|
|
29
|
+
register: remote_env
|
|
24
30
|
|
|
25
31
|
- name: "[1/5] Fail if .env missing"
|
|
26
32
|
fail:
|
|
27
33
|
msg: ".env file not found at /opt/gravity/.env - run install.yml first"
|
|
28
|
-
when: not
|
|
34
|
+
when: not remote_env.stat.exists
|
|
29
35
|
|
|
30
36
|
- name: "[2/5] Check DATABASE_URL is configured"
|
|
31
37
|
shell: grep -q "^DATABASE_URL=" /opt/gravity/.env
|
|
@@ -126,15 +126,12 @@
|
|
|
126
126
|
owner: "{{ ansible_user }}"
|
|
127
127
|
mode: "0644"
|
|
128
128
|
|
|
129
|
-
#
|
|
130
|
-
#
|
|
131
|
-
#
|
|
132
|
-
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
regexp: '^\s*-\s*UNOVERSE_WORKBENCH=1\s*$'
|
|
136
|
-
replace: ' - UNOVERSE_WORKBENCH=0'
|
|
137
|
-
|
|
129
|
+
# NO WORKBENCH STEP HERE. This used to force UNOVERSE_WORKBENCH=0 in the deployed
|
|
130
|
+
# compose, back when Studio shipped inside the platform image and had to be switched
|
|
131
|
+
# off for production. Studio is a separate CLI-launched app now and the variable is
|
|
132
|
+
# gone from docker-compose.yml entirely, so the task matched nothing and reported "ok"
|
|
133
|
+
# anyway — a step that proves a safeguard is working while doing nothing at all is
|
|
134
|
+
# worse than no step.
|
|
138
135
|
- name: "[8/12] Copy .env file"
|
|
139
136
|
copy:
|
|
140
137
|
# Same trap as the compose above: a path relative to the playbook is the monorepo's
|
package/operator/lib/deploy.sh
CHANGED
|
@@ -302,7 +302,19 @@ _ground_apply() {
|
|
|
302
302
|
|
|
303
303
|
cmd_deploy() {
|
|
304
304
|
|
|
305
|
-
|
|
305
|
+
# .env.production IS NOT A FILE IN THE UNIVERSE. It is `terraform output -raw
|
|
306
|
+
# env_production`: derived, complete, and regenerated in full on every deploy. Writing it
|
|
307
|
+
# to the universe root put a second env file next to `.env`, which reads like something
|
|
308
|
+
# to edit and is not — the next deploy overwrites it — and it outlived the server it
|
|
309
|
+
# described, so a torn-down universe still looked deployed. Render it to a private temp
|
|
310
|
+
# file, ship it, delete it. The ground stays the only source of truth.
|
|
311
|
+
local env_prod
|
|
312
|
+
env_prod=$(mktemp -t unoverse-env-prod) || exit 1
|
|
313
|
+
chmod 600 "$env_prod"
|
|
314
|
+
trap 'rm -f "$env_prod" 2>/dev/null' EXIT
|
|
315
|
+
# Older universes have one on disk. It is stale by definition now, and leaving it means
|
|
316
|
+
# the confusing second env file simply never goes away.
|
|
317
|
+
rm -f "$ROOT/.env.production"
|
|
306
318
|
|
|
307
319
|
_ground_credentials
|
|
308
320
|
|
|
@@ -343,26 +355,22 @@ cmd_deploy() {
|
|
|
343
355
|
|
|
344
356
|
_ground_apply "$cloud"; rc=$?
|
|
345
357
|
[ "$rc" = "1" ] && exit 1
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
fi
|
|
350
|
-
|
|
351
|
-
if [ ! -f "$env_prod" ]; then
|
|
352
|
-
fail ".env.production could not be rendered — has the ground been applied?"
|
|
358
|
+
terraform -chdir="$ROOT/infra/$cloud" output -raw env_production > "$env_prod" 2>/dev/null
|
|
359
|
+
if [ ! -s "$env_prod" ]; then
|
|
360
|
+
fail "Could not read this universe's settings from your $cloud ground. Has it been applied?"
|
|
353
361
|
exit 1
|
|
354
362
|
fi
|
|
355
363
|
|
|
356
364
|
_adopted_db_access "$cloud" grant
|
|
357
365
|
|
|
358
366
|
|
|
359
|
-
# Read deploy target from
|
|
367
|
+
# Read the deploy target from the rendered settings
|
|
360
368
|
local deploy_host deploy_user
|
|
361
369
|
deploy_host=$(grep '^DEPLOY_HOST=' "$env_prod" | cut -d= -f2- | tr -d '\r\n' | xargs)
|
|
362
370
|
deploy_user=$(grep '^DEPLOY_USER=' "$env_prod" | cut -d= -f2- | tr -d '\r\n' | xargs)
|
|
363
371
|
|
|
364
372
|
if [ -z "$deploy_host" ] || [ "$deploy_host" = "your-vm-ip" ]; then
|
|
365
|
-
fail "
|
|
373
|
+
fail "This ground has no server address yet. Has it been applied?"
|
|
366
374
|
exit 1
|
|
367
375
|
fi
|
|
368
376
|
deploy_user="${deploy_user:-root}"
|
|
@@ -379,7 +387,7 @@ cmd_deploy() {
|
|
|
379
387
|
fi
|
|
380
388
|
ok "Ansible available"
|
|
381
389
|
|
|
382
|
-
# Generate a temporary inventory from
|
|
390
|
+
# Generate a temporary inventory from the rendered settings
|
|
383
391
|
local tmp_inventory
|
|
384
392
|
tmp_inventory=$(mktemp).yml
|
|
385
393
|
cat > "$tmp_inventory" << 'EOF'
|
|
@@ -421,11 +429,17 @@ EOF
|
|
|
421
429
|
# install.yml creates. The remedy was to know that `unoverse deploy init` exists, which
|
|
422
430
|
# is exactly the kind of knowledge this CLI is supposed to remove: deploy owns the whole
|
|
423
431
|
# journey, so it asks the server what it is and picks the right playbook itself.
|
|
432
|
+
# THE MARKER IS "SETUP FINISHED", NOT "A DIRECTORY EXISTS". Testing for /opt/gravity
|
|
433
|
+
# looked right and was not: install.yml creates that directory as its seventh task, so a
|
|
434
|
+
# first-time setup that then FAILED at the database step still left it behind. The next
|
|
435
|
+
# deploy saw it, decided the server was ready, shipped images onto a database that had
|
|
436
|
+
# never been migrated, and reported success. The stamp is written only after install,
|
|
437
|
+
# database and verify have all passed, so an interrupted setup resumes as a setup.
|
|
424
438
|
if [ -z "$subcommand" ]; then
|
|
425
439
|
if ! ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=15 \
|
|
426
|
-
"$deploy_user@$deploy_host" 'test -
|
|
440
|
+
"$deploy_user@$deploy_host" 'test -f /opt/gravity/.setup-complete' >/dev/null 2>&1; then
|
|
427
441
|
echo ""
|
|
428
|
-
info "
|
|
442
|
+
info "This server's setup has not finished. Running it ${DIM}(install, database, verify)${NC}"
|
|
429
443
|
subcommand="init"
|
|
430
444
|
fi
|
|
431
445
|
fi
|
|
@@ -470,6 +484,9 @@ EOF
|
|
|
470
484
|
"$ansible_dir/playbooks/test-connectivity.yml" \
|
|
471
485
|
-e "universe_root=$ROOT" \
|
|
472
486
|
-e "env_file=$env_prod" || { rm -f "$tmp_inventory"; fail "verification failed — inspect and re-run: unoverse deploy test"; exit 1; }
|
|
487
|
+
# Only now. Every step passed, so the next deploy can safely be an image push.
|
|
488
|
+
ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=15 \
|
|
489
|
+
"$deploy_user@$deploy_host" 'touch /opt/gravity/.setup-complete' >/dev/null 2>&1
|
|
473
490
|
echo ""
|
|
474
491
|
ok "Your universe is up. From now on, deploys are just: unoverse deploy"
|
|
475
492
|
info "Keeping it? Harden the VM when you're ready: unoverse deploy harden"
|
package/package.json
CHANGED