unoverse 0.1.69 → 0.1.71
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/bin/unoverse.mjs +1 -1
- package/lib/create/credential.mjs +90 -0
- package/lib/create/download.mjs +19 -0
- package/lib/create/index.mjs +141 -0
- package/lib/create/target.mjs +40 -0
- package/lib/ui.mjs +119 -0
- package/lib/urls.mjs +1 -7
- package/operator/infra/aws/main.tf +639 -0
- package/operator/infra/aws/outputs.tf +139 -0
- package/operator/infra/aws/pretoken/index.mjs +37 -0
- package/operator/infra/aws/prices.tf +53 -0
- package/operator/infra/aws/terraform.tfvars.example +55 -0
- package/operator/infra/aws/variables.tf +92 -0
- package/operator/infra/digitalocean/main.tf +297 -0
- package/operator/infra/digitalocean/outputs.tf +96 -0
- package/operator/infra/digitalocean/prices.tf +49 -0
- package/operator/infra/digitalocean/terraform.tfvars.example +52 -0
- package/operator/infra/digitalocean/variables.tf +125 -0
- package/operator/lib/deploy.sh +151 -160
- package/operator/lib/init.sh +2 -9
- package/operator/operator.sh +17 -0
- package/package.json +1 -1
- package/lib/create.mjs +0 -369
package/operator/lib/deploy.sh
CHANGED
|
@@ -11,17 +11,129 @@
|
|
|
11
11
|
# Nothing is softened. Destroys are shouted, the full technical plan is one keystroke
|
|
12
12
|
# away, and the SAVED plan is what runs, so what was approved is what happens. Identical
|
|
13
13
|
# for every cloud: the summary reads the plan JSON, which is provider-agnostic.
|
|
14
|
+
# Terraform is one static binary: install it from the official release rather than
|
|
15
|
+
# sending anyone to brew, whose compile chain can demand Xcode Command Line Tools
|
|
16
|
+
# updates for a tool that needs no compiling.
|
|
17
|
+
_ensure_terraform() {
|
|
18
|
+
command -v terraform >/dev/null 2>&1 && return 0
|
|
19
|
+
local REPLY tf_arch tf_os tf_v="1.9.8" tf_dir
|
|
20
|
+
read -r -p " Terraform is needed. Install it now (official binary, ~30 MB)? [Y/n] " REPLY
|
|
21
|
+
[[ "$REPLY" =~ ^[Nn]$ ]] && { fail "terraform is needed to continue"; return 1; }
|
|
22
|
+
tf_arch=$(uname -m)
|
|
23
|
+
case "$tf_arch" in arm64|aarch64) tf_arch=arm64 ;; *) tf_arch=amd64 ;; esac
|
|
24
|
+
[ "$(uname -s)" = "Darwin" ] && tf_os=darwin || tf_os=linux
|
|
25
|
+
tf_dir=$(mktemp -d)
|
|
26
|
+
curl -fsSL -o "$tf_dir/tf.zip" "https://releases.hashicorp.com/terraform/${tf_v}/terraform_${tf_v}_${tf_os}_${tf_arch}.zip" \
|
|
27
|
+
&& unzip -o -q "$tf_dir/tf.zip" -d "$tf_dir" || { fail "download failed"; rm -rf "$tf_dir"; return 1; }
|
|
28
|
+
if [ -w /usr/local/bin ]; then mv "$tf_dir/terraform" /usr/local/bin/terraform
|
|
29
|
+
else sudo mv "$tf_dir/terraform" /usr/local/bin/terraform || { fail "could not install to /usr/local/bin"; rm -rf "$tf_dir"; return 1; }
|
|
30
|
+
fi
|
|
31
|
+
rm -rf "$tf_dir"
|
|
32
|
+
ok "terraform $(terraform version | head -1 | awk '{print $2}') installed"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Everything that must be TRUE before a plan is worth looking at: every value filled,
|
|
37
|
+
# and the database decision made. It lives in a function because deploy has one flow now
|
|
38
|
+
# and this step belongs in it — it used to sit inside the "no .env.production yet" branch,
|
|
39
|
+
# so an already-initialised ground skipped straight to planning and the Postgres question
|
|
40
|
+
# was never asked. Two paths, and the configuration existed in only one of them.
|
|
41
|
+
_ensure_ground_config() {
|
|
42
|
+
local cloud="$1"
|
|
43
|
+
local tfv="$ROOT/infra/$cloud/terraform.tfvars"
|
|
44
|
+
_tf_put() { # key value — safe replacement via node (values may hold sed specials)
|
|
45
|
+
node -e 'const fs=require("fs");const[f,k,v]=process.argv.slice(1);let s=fs.readFileSync(f,"utf8");s=s.replace(new RegExp("^"+k+"(\\s*)=\\s*\"FILL_ME\"","m"),k+"$1= "+JSON.stringify(v));fs.writeFileSync(f,s)' "$tfv" "$1" "$2"
|
|
46
|
+
}
|
|
47
|
+
_tf_fill() { # key envname prompt
|
|
48
|
+
local key="$1" envname="$2" prompt="$3" val
|
|
49
|
+
grep -q "^${key}[[:space:]]*=[[:space:]]*\"FILL_ME\"" "$tfv" 2>/dev/null || return 0
|
|
50
|
+
val=$(grep "^${envname}=" "$ROOT/.env" 2>/dev/null | head -1 | cut -d= -f2-)
|
|
51
|
+
if [ -n "$val" ]; then
|
|
52
|
+
_tf_put "$key" "$val"
|
|
53
|
+
ok "$key ${DIM}from your .env${NC}"
|
|
54
|
+
else
|
|
55
|
+
read -r -p " $prompt: " val
|
|
56
|
+
[ -n "$val" ] && _tf_put "$key" "$val"
|
|
57
|
+
fi
|
|
58
|
+
}
|
|
59
|
+
echo ""
|
|
60
|
+
# POSTGRES: ASK, DO NOT ASSUME. The ground discovers an existing cluster and writes
|
|
61
|
+
# it as a COMMENTED line, so the default silently provisions a SECOND database next
|
|
62
|
+
# to one the account already pays for. A choice with a monthly bill attached is a
|
|
63
|
+
# question, not a line to notice in a file.
|
|
64
|
+
if [ "$cloud" = "digitalocean" ] \
|
|
65
|
+
&& grep -q '^#[[:space:]]*existing_pg_cluster_name' "$tfv" 2>/dev/null \
|
|
66
|
+
&& ! grep -q '^existing_pg_cluster_name' "$tfv" 2>/dev/null \
|
|
67
|
+
&& ! grep -q '^byo_postgres_url' "$tfv" 2>/dev/null; then
|
|
68
|
+
local found
|
|
69
|
+
found=$(grep '^#[[:space:]]*existing_pg_cluster_name' "$tfv" | sed -E 's/.*"([^"]+)".*/\1/')
|
|
70
|
+
if [ -n "$found" ]; then
|
|
71
|
+
echo ""
|
|
72
|
+
echo -e " ${CYAN}${BOLD}You already have a database.${NC} ${DIM}$found${NC}"
|
|
73
|
+
echo ""
|
|
74
|
+
pick_option "Use it" "Create a new one ~\$15/month"
|
|
75
|
+
if [ "$PICKED" = "0" ]; then
|
|
76
|
+
node -e 'const fs=require("fs");const[f,c]=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" "$found"
|
|
77
|
+
ok "Reusing $found"
|
|
78
|
+
else
|
|
79
|
+
ok "A new database will be created"
|
|
80
|
+
fi
|
|
81
|
+
fi
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
_tf_fill docr_token DOCR_TOKEN "Registry access token (from your Unoverse admin)"
|
|
85
|
+
_tf_fill openai_api_key OPENAI_API_KEY "OPENAI_API_KEY (powers the platform's AI)"
|
|
86
|
+
if grep -q '^auth_issuer[[:space:]]*=[[:space:]]*"FILL_ME"' "$tfv" 2>/dev/null; then
|
|
87
|
+
local env_auth
|
|
88
|
+
env_auth=$(grep "^AUTH_ISSUER=" "$ROOT/.env" 2>/dev/null | cut -d= -f2-)
|
|
89
|
+
if [ -z "$env_auth" ]; then
|
|
90
|
+
echo ""
|
|
91
|
+
info "A deployed universe requires a login. Local auth-off does not deploy"
|
|
92
|
+
info "${DIM}(Auth0 or any OIDC provider; the issuer looks like https://your-tenant.auth0.com)${NC}"
|
|
93
|
+
fi
|
|
94
|
+
fi
|
|
95
|
+
_tf_fill auth_issuer AUTH_ISSUER "AUTH_ISSUER"
|
|
96
|
+
_tf_fill auth_client_id AUTH_CLIENT_ID "AUTH_CLIENT_ID"
|
|
97
|
+
|
|
98
|
+
# VALUES only: the file's own header comment says the word FILL_ME, and matching
|
|
99
|
+
# it declared a complete file blank.
|
|
100
|
+
if grep -qE '=[[:space:]]*"FILL_ME"' "$tfv" 2>/dev/null; then
|
|
101
|
+
echo ""
|
|
102
|
+
warn "Some values are still blank in ${BOLD}infra/$cloud/terraform.tfvars${NC} — fill them, then:"
|
|
103
|
+
info " cd infra/$cloud && terraform init && terraform apply"
|
|
104
|
+
info " cd ../.. && unoverse deploy"
|
|
105
|
+
echo ""
|
|
106
|
+
exit 1
|
|
107
|
+
fi
|
|
108
|
+
ok "terraform.tfvars is complete"
|
|
109
|
+
|
|
110
|
+
# SIZE IS NOT A ONE-WAY DOOR. small is the POC box, and the plan below is about to
|
|
111
|
+
# quote its monthly cost — the moment someone wonders whether they are committing to
|
|
112
|
+
# it. One line, here, rather than a discovery later.
|
|
113
|
+
local cur_size
|
|
114
|
+
cur_size=$(grep -E '^size[[:space:]]*=' "$tfv" 2>/dev/null | sed -E 's/.*"([^"]+)".*/\1/')
|
|
115
|
+
[ -n "$cur_size" ] && info "Size: ${BOLD}${cur_size}${NC} ${DIM}(small · medium · large — change it in terraform.tfvars and deploy again whenever you outgrow it)${NC}"
|
|
116
|
+
|
|
117
|
+
# STRAIGHT INTO TERRAFORM. Its plan plus its own typed "yes" IS the decision gate
|
|
118
|
+
# for billable infrastructure; a [Y/n] in front of it was asking permission to ask
|
|
119
|
+
# permission. Answering "no" is terraform's "no".
|
|
120
|
+
echo ""
|
|
121
|
+
|
|
122
|
+
}
|
|
123
|
+
|
|
14
124
|
_ground_apply() {
|
|
15
125
|
local cloud="$1" tmp planfile rc
|
|
16
126
|
# SEPARATE STATEMENT. On macOS bash 3.2 a single `local a="$1" b="...$a"` expands $a
|
|
17
127
|
# EMPTY, so dir became "$ROOT/infra/" and terraform answered "No configuration files".
|
|
128
|
+
|
|
129
|
+
|
|
18
130
|
# Same trap that once made the release publish the repo root.
|
|
19
131
|
local dir="$ROOT/infra/$cloud"
|
|
20
132
|
tmp=$(mktemp -d); planfile="$tmp/plan"
|
|
21
133
|
|
|
22
134
|
terraform -chdir="$dir" init -input=false >/dev/null 2>&1 || { fail "terraform init failed"; rm -rf "$tmp"; return 1; }
|
|
23
135
|
|
|
24
|
-
info "Working out what needs to change..."
|
|
136
|
+
info "Working out what needs to change ${DIM}($cloud)${NC}..."
|
|
25
137
|
terraform -chdir="$dir" plan -input=false -detailed-exitcode -out="$planfile" >"$tmp/plan.log" 2>&1
|
|
26
138
|
rc=$?
|
|
27
139
|
case "$rc" in
|
|
@@ -34,8 +146,11 @@ _ground_apply() {
|
|
|
34
146
|
terraform -chdir="$dir" show "$planfile"
|
|
35
147
|
fi
|
|
36
148
|
|
|
149
|
+
# `read -p` does NOT interpret escapes, so ${DIM} printed as literal \033[2m. Colour
|
|
150
|
+
# the hint with echo -e first, then take a plain prompt.
|
|
37
151
|
local REPLY
|
|
38
|
-
|
|
152
|
+
echo -e " ${DIM}(d shows the full technical plan)${NC}"
|
|
153
|
+
read -r -p " Go ahead? [y/N] " REPLY
|
|
39
154
|
if [[ "$REPLY" =~ ^[Dd]$ ]]; then
|
|
40
155
|
terraform -chdir="$dir" show "$planfile"
|
|
41
156
|
read -r -p " Go ahead? [y/N] " REPLY
|
|
@@ -70,178 +185,54 @@ cmd_deploy() {
|
|
|
70
185
|
done
|
|
71
186
|
fi
|
|
72
187
|
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
local g rc
|
|
81
|
-
for g in digitalocean aws; do
|
|
82
|
-
[ -d "$ROOT/infra/$g/.terraform" ] || continue
|
|
83
|
-
_ground_apply "$g"; rc=$?
|
|
84
|
-
[ "$rc" = "1" ] && exit 1
|
|
85
|
-
if [ "$rc" = "0" ]; then
|
|
86
|
-
terraform -chdir="$ROOT/infra/$g" output -raw env_production > "$env_prod" 2>/dev/null \
|
|
87
|
-
&& ok "Refreshed .env.production from the ground"
|
|
88
|
-
g_applied=1
|
|
89
|
-
fi
|
|
90
|
-
break
|
|
91
|
-
done
|
|
92
|
-
fi
|
|
93
|
-
|
|
94
|
-
# Missing .env.production is not an error if a ground has been applied — the
|
|
95
|
-
# file is just a Terraform output, so render it ourselves. Reading state is
|
|
96
|
-
# not wrapping terraform: apply stays the developer's explicit act.
|
|
97
|
-
if [ ! -f "$env_prod" ] && command -v terraform >/dev/null 2>&1; then
|
|
98
|
-
local g
|
|
99
|
-
for g in digitalocean aws; do
|
|
100
|
-
[ -d "$ROOT/infra/$g" ] || continue
|
|
101
|
-
if terraform -chdir="$ROOT/infra/$g" output -raw env_production > "$env_prod.tmp" 2>/dev/null \
|
|
102
|
-
&& grep -q '^DEPLOY_HOST=' "$env_prod.tmp"; then
|
|
103
|
-
mv "$env_prod.tmp" "$env_prod"
|
|
104
|
-
ok "Rendered .env.production from your $g ground"
|
|
105
|
-
info "It holds CREDENTIAL_ENCRYPTION_KEY — back it up with the DB, never commit it."
|
|
106
|
-
break
|
|
107
|
-
fi
|
|
108
|
-
rm -f "$env_prod.tmp"
|
|
109
|
-
done
|
|
110
|
-
fi
|
|
188
|
+
# ── ONE FLOW ────────────────────────────────────────────────────────────────
|
|
189
|
+
# Which ground, is it configured, plan it, apply it, ship it. In that order, every
|
|
190
|
+
# time, whether this is the first deploy or the fiftieth.
|
|
191
|
+
local cloud="" g rc
|
|
192
|
+
for g in digitalocean aws; do
|
|
193
|
+
[ -f "$ROOT/infra/$g/terraform.tfvars" ] && { cloud="$g"; break; }
|
|
194
|
+
done
|
|
111
195
|
|
|
112
|
-
|
|
113
|
-
# journey: pick the cloud right here (ground is no longer a command to know about),
|
|
114
|
-
# prefill its Terraform, and name the two commands left. `terraform apply` stays the
|
|
115
|
-
# developer's own explicit act — it creates billable infrastructure.
|
|
116
|
-
if [ ! -f "$env_prod" ]; then
|
|
196
|
+
if [ -z "$cloud" ]; then
|
|
117
197
|
echo ""
|
|
118
198
|
echo -e " ${CYAN}${BOLD}Where should this universe live?${NC}"
|
|
119
199
|
echo ""
|
|
120
|
-
local cloud
|
|
121
200
|
pick_option "DigitalOcean" "AWS"
|
|
122
201
|
[ "$PICKED" = "1" ] && cloud=aws || cloud=digitalocean
|
|
123
202
|
echo ""
|
|
124
|
-
|
|
125
|
-
# a missing file needs the ground; either way the flow continues below.
|
|
126
|
-
if [ -f "$ROOT/infra/$cloud/terraform.tfvars" ]; then
|
|
127
|
-
ok "Using your existing infra/$cloud/terraform.tfvars"
|
|
128
|
-
else
|
|
129
|
-
cmd_ground "$([ "$cloud" = "aws" ] && echo aws || echo do)" || {
|
|
130
|
-
echo ""
|
|
131
|
-
info "Once that is sorted, run ${BOLD}unoverse deploy${NC} again. It picks up right here."
|
|
132
|
-
echo ""
|
|
133
|
-
exit 1
|
|
134
|
-
}
|
|
135
|
-
fi
|
|
136
|
-
echo ""
|
|
137
|
-
ok "Your $cloud ground is prefilled ${DIM}(infra/$cloud/terraform.tfvars)${NC}"
|
|
138
|
-
|
|
139
|
-
# FILL THE BLANKS OURSELVES. Every FILL_ME the universe already knows (.env from
|
|
140
|
-
# setup) is copied in silently; only genuine unknowns are asked. Hand-copying
|
|
141
|
-
# values between our own files is not a step a developer should ever see.
|
|
142
|
-
local tfv="$ROOT/infra/$cloud/terraform.tfvars"
|
|
143
|
-
_tf_put() { # key value — safe replacement via node (values may hold sed specials)
|
|
144
|
-
node -e 'const fs=require("fs");const[f,k,v]=process.argv.slice(1);let s=fs.readFileSync(f,"utf8");s=s.replace(new RegExp("^"+k+"(\\s*)=\\s*\"FILL_ME\"","m"),k+"$1= "+JSON.stringify(v));fs.writeFileSync(f,s)' "$tfv" "$1" "$2"
|
|
145
|
-
}
|
|
146
|
-
_tf_fill() { # key envname prompt
|
|
147
|
-
local key="$1" envname="$2" prompt="$3" val
|
|
148
|
-
grep -q "^${key}[[:space:]]*=[[:space:]]*\"FILL_ME\"" "$tfv" 2>/dev/null || return 0
|
|
149
|
-
val=$(grep "^${envname}=" "$ROOT/.env" 2>/dev/null | head -1 | cut -d= -f2-)
|
|
150
|
-
if [ -n "$val" ]; then
|
|
151
|
-
_tf_put "$key" "$val"
|
|
152
|
-
ok "$key ${DIM}from your .env${NC}"
|
|
153
|
-
else
|
|
154
|
-
read -r -p " $prompt: " val
|
|
155
|
-
[ -n "$val" ] && _tf_put "$key" "$val"
|
|
156
|
-
fi
|
|
157
|
-
}
|
|
158
|
-
echo ""
|
|
159
|
-
# POSTGRES: ASK, DO NOT ASSUME. The ground discovers an existing cluster and writes
|
|
160
|
-
# it as a COMMENTED line, so the default silently provisions a SECOND database next
|
|
161
|
-
# to one the account already pays for. A choice with a monthly bill attached is a
|
|
162
|
-
# question, not a line to notice in a file.
|
|
163
|
-
if [ "$cloud" = "digitalocean" ] \
|
|
164
|
-
&& grep -q '^#[[:space:]]*existing_pg_cluster_name' "$tfv" 2>/dev/null \
|
|
165
|
-
&& ! grep -q '^existing_pg_cluster_name' "$tfv" 2>/dev/null \
|
|
166
|
-
&& ! grep -q '^byo_postgres_url' "$tfv" 2>/dev/null; then
|
|
167
|
-
local found
|
|
168
|
-
found=$(grep '^#[[:space:]]*existing_pg_cluster_name' "$tfv" | sed -E 's/.*"([^"]+)".*/\1/')
|
|
169
|
-
if [ -n "$found" ]; then
|
|
170
|
-
echo ""
|
|
171
|
-
echo -e " ${CYAN}${BOLD}You already have a database.${NC} ${DIM}$found${NC}"
|
|
172
|
-
echo ""
|
|
173
|
-
pick_option "Use it" "Create a new one ~\$15/month"
|
|
174
|
-
if [ "$PICKED" = "0" ]; then
|
|
175
|
-
node -e 'const fs=require("fs");const[f,c]=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" "$found"
|
|
176
|
-
ok "Reusing $found"
|
|
177
|
-
else
|
|
178
|
-
ok "A new database will be created"
|
|
179
|
-
fi
|
|
180
|
-
fi
|
|
181
|
-
fi
|
|
182
|
-
|
|
183
|
-
_tf_fill docr_token DOCR_TOKEN "Registry access token (from your Unoverse admin)"
|
|
184
|
-
_tf_fill openai_api_key OPENAI_API_KEY "OPENAI_API_KEY (powers the platform's AI)"
|
|
185
|
-
if grep -q '^auth_issuer[[:space:]]*=[[:space:]]*"FILL_ME"' "$tfv" 2>/dev/null; then
|
|
186
|
-
local env_auth
|
|
187
|
-
env_auth=$(grep "^AUTH_ISSUER=" "$ROOT/.env" 2>/dev/null | cut -d= -f2-)
|
|
188
|
-
if [ -z "$env_auth" ]; then
|
|
189
|
-
echo ""
|
|
190
|
-
info "A deployed universe requires a login. Local auth-off does not deploy"
|
|
191
|
-
info "${DIM}(Auth0 or any OIDC provider; the issuer looks like https://your-tenant.auth0.com)${NC}"
|
|
192
|
-
fi
|
|
193
|
-
fi
|
|
194
|
-
_tf_fill auth_issuer AUTH_ISSUER "AUTH_ISSUER"
|
|
195
|
-
_tf_fill auth_client_id AUTH_CLIENT_ID "AUTH_CLIENT_ID"
|
|
196
|
-
|
|
197
|
-
# VALUES only: the file's own header comment says the word FILL_ME, and matching
|
|
198
|
-
# it declared a complete file blank.
|
|
199
|
-
if grep -qE '=[[:space:]]*"FILL_ME"' "$tfv" 2>/dev/null; then
|
|
203
|
+
cmd_ground "$([ "$cloud" = "aws" ] && echo aws || echo do)" || {
|
|
200
204
|
echo ""
|
|
201
|
-
|
|
202
|
-
info " cd infra/$cloud && terraform init && terraform apply"
|
|
203
|
-
info " cd ../.. && unoverse deploy"
|
|
205
|
+
info "Once that is sorted, run ${BOLD}unoverse deploy${NC} again. It picks up right here."
|
|
204
206
|
echo ""
|
|
205
207
|
exit 1
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
#
|
|
210
|
-
#
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
cur_size=$(grep -E '^size[[:space:]]*=' "$tfv" 2>/dev/null | sed -E 's/.*"([^"]+)".*/\1/')
|
|
214
|
-
[ -n "$cur_size" ] && info "Size: ${BOLD}${cur_size}${NC} ${DIM}(small · medium · large — change it in terraform.tfvars and deploy again whenever you outgrow it)${NC}"
|
|
215
|
-
|
|
216
|
-
# STRAIGHT INTO TERRAFORM. Its plan plus its own typed "yes" IS the decision gate
|
|
217
|
-
# for billable infrastructure; a [Y/n] in front of it was asking permission to ask
|
|
218
|
-
# permission. Answering "no" is terraform's "no".
|
|
219
|
-
echo ""
|
|
220
|
-
# Terraform is one static binary: install it directly from the official release
|
|
221
|
-
# rather than sending anyone to brew (whose compile chain can demand Xcode Command
|
|
222
|
-
# Line Tools updates for a tool that needs no compiling).
|
|
223
|
-
if ! command -v terraform >/dev/null 2>&1; then
|
|
224
|
-
local REPLY2 tf_arch tf_os tf_v="1.9.8" tf_dir
|
|
225
|
-
read -r -p " Terraform is needed. Install it now (official binary, ~30 MB)? [Y/n] " REPLY2
|
|
226
|
-
[[ "$REPLY2" =~ ^[Nn]$ ]] && { fail "terraform is needed to continue"; exit 1; }
|
|
227
|
-
tf_arch=$(uname -m); [ "$tf_arch" = "arm64" ] || [ "$tf_arch" = "aarch64" ] && tf_arch=arm64 || tf_arch=amd64
|
|
228
|
-
[ "$(uname -s)" = "Darwin" ] && tf_os=darwin || tf_os=linux
|
|
229
|
-
tf_dir=$(mktemp -d)
|
|
230
|
-
curl -fsSL -o "$tf_dir/tf.zip" "https://releases.hashicorp.com/terraform/${tf_v}/terraform_${tf_v}_${tf_os}_${tf_arch}.zip" && unzip -o -q "$tf_dir/tf.zip" -d "$tf_dir" || { fail "download failed"; exit 1; }
|
|
231
|
-
if [ -w /usr/local/bin ]; then mv "$tf_dir/terraform" /usr/local/bin/terraform
|
|
232
|
-
else sudo mv "$tf_dir/terraform" /usr/local/bin/terraform || { fail "could not install to /usr/local/bin"; exit 1; }
|
|
233
|
-
fi
|
|
234
|
-
rm -rf "$tf_dir"
|
|
235
|
-
ok "terraform $(terraform version | head -1 | awk '{print $2}') installed"
|
|
236
|
-
fi
|
|
237
|
-
_ground_apply "$cloud" || exit 1
|
|
208
|
+
}
|
|
209
|
+
else
|
|
210
|
+
# SAY WHERE THIS IS GOING, first and unmissably. An existing ground already names
|
|
211
|
+
# the cloud, so re-asking every deploy would be noise — but a one-line mention
|
|
212
|
+
# buried above other output reads as never having been asked at all.
|
|
213
|
+
local pretty
|
|
214
|
+
[ "$cloud" = "aws" ] && pretty="AWS" || pretty="DigitalOcean"
|
|
238
215
|
echo ""
|
|
239
|
-
|
|
216
|
+
echo -e " ${CYAN}${BOLD}⬡ Deploying to $pretty${NC} ${DIM}(infra/$cloud — delete that folder's terraform.tfvars to choose again)${NC}"
|
|
240
217
|
echo ""
|
|
241
|
-
cmd_deploy init
|
|
242
|
-
return $?
|
|
243
218
|
fi
|
|
244
219
|
|
|
220
|
+
_ensure_ground_config "$cloud" || exit 1
|
|
221
|
+
_ensure_terraform || exit 1
|
|
222
|
+
|
|
223
|
+
_ground_apply "$cloud"; rc=$?
|
|
224
|
+
[ "$rc" = "1" ] && exit 1
|
|
225
|
+
if [ "$rc" = "0" ] || [ ! -f "$env_prod" ]; then
|
|
226
|
+
terraform -chdir="$ROOT/infra/$cloud" output -raw env_production > "$env_prod" 2>/dev/null \
|
|
227
|
+
&& ok "Rendered .env.production from your $cloud ground"
|
|
228
|
+
fi
|
|
229
|
+
|
|
230
|
+
if [ ! -f "$env_prod" ]; then
|
|
231
|
+
fail ".env.production could not be rendered — has the ground been applied?"
|
|
232
|
+
exit 1
|
|
233
|
+
fi
|
|
234
|
+
|
|
235
|
+
|
|
245
236
|
# Read deploy target from .env.production
|
|
246
237
|
local deploy_host deploy_user
|
|
247
238
|
deploy_host=$(grep '^DEPLOY_HOST=' "$env_prod" | cut -d= -f2- | tr -d '\r\n' | xargs)
|
package/operator/lib/init.sh
CHANGED
|
@@ -334,7 +334,7 @@ cmd_init() {
|
|
|
334
334
|
|
|
335
335
|
# Node vendor keys, OPTIONAL: only the matching nodes need them, nothing platform-level
|
|
336
336
|
# does. Asked so compose stops warning about them and so they land in .env with names.
|
|
337
|
-
local cur_hb
|
|
337
|
+
local cur_hb
|
|
338
338
|
cur_hb=$(_env_cur HYPERBROWSER_API_KEY)
|
|
339
339
|
if [ -n "$cur_hb" ]; then
|
|
340
340
|
read -p " HYPERBROWSER_API_KEY [keep current]: " HYPERBROWSER_API_KEY
|
|
@@ -342,13 +342,7 @@ cmd_init() {
|
|
|
342
342
|
else
|
|
343
343
|
read -p " HYPERBROWSER_API_KEY (browser nodes, blank to skip): " HYPERBROWSER_API_KEY
|
|
344
344
|
fi
|
|
345
|
-
|
|
346
|
-
if [ -n "$cur_sa" ]; then
|
|
347
|
-
read -p " SEARCHAPI_KEY [keep current]: " SEARCHAPI_KEY
|
|
348
|
-
SEARCHAPI_KEY="${SEARCHAPI_KEY:-$cur_sa}"
|
|
349
|
-
else
|
|
350
|
-
read -p " SEARCHAPI_KEY (web-search nodes, blank to skip): " SEARCHAPI_KEY
|
|
351
|
-
fi
|
|
345
|
+
|
|
352
346
|
|
|
353
347
|
# A silent fact, not a question: the encryption key is GENERATED (a human never types
|
|
354
348
|
# one) and kept verbatim on re-runs — a new key orphans every stored credential.
|
|
@@ -378,7 +372,6 @@ OPENAI_API_KEY=${OPENAI_API_KEY}
|
|
|
378
372
|
# back it up with the database. Kept on re-runs.
|
|
379
373
|
CREDENTIAL_ENCRYPTION_KEY=${CREDENTIAL_ENCRYPTION_KEY}
|
|
380
374
|
HYPERBROWSER_API_KEY=${HYPERBROWSER_API_KEY}
|
|
381
|
-
SEARCHAPI_KEY=${SEARCHAPI_KEY}
|
|
382
375
|
DOMAIN=
|
|
383
376
|
ENVEOF
|
|
384
377
|
|
package/operator/operator.sh
CHANGED
|
@@ -73,6 +73,23 @@ case "${1:-}" in
|
|
|
73
73
|
elif ! docker info >/dev/null 2>&1; then
|
|
74
74
|
info "Docker is not running. Images refresh on the next unoverse start"
|
|
75
75
|
else
|
|
76
|
+
# The ground definitions first: they are code, and they update like code.
|
|
77
|
+
# NO `local` HERE: a case branch in the dispatch is not a function body, and bash
|
|
78
|
+
# errors "local: can only be used in a function" straight to the developer's screen.
|
|
79
|
+
_vinfra="$GRAVITY_LIB/../infra"
|
|
80
|
+
if [ -d "$_vinfra" ]; then
|
|
81
|
+
for _g in digitalocean aws; do
|
|
82
|
+
[ -d "$ROOT/infra/$_g" ] && [ -d "$_vinfra/$_g" ] || continue
|
|
83
|
+
_changed=0
|
|
84
|
+
for _f in "$_vinfra/$_g"/*.tf "$_vinfra/$_g"/*.example; do
|
|
85
|
+
[ -f "$_f" ] || continue
|
|
86
|
+
_base=$(basename "$_f")
|
|
87
|
+
cmp -s "$_f" "$ROOT/infra/$_g/$_base" || { cp "$_f" "$ROOT/infra/$_g/$_base"; _changed=1; }
|
|
88
|
+
done
|
|
89
|
+
[ "$_changed" = "1" ] && ok "Refreshed the $_g ground ${DIM}(your terraform.tfvars and state untouched)${NC}"
|
|
90
|
+
done
|
|
91
|
+
fi
|
|
92
|
+
|
|
76
93
|
info "Checking for newer platform images..."
|
|
77
94
|
if ! docker compose -f "$ROOT/docker-compose.yml" --env-file "$ROOT/.env" pull; then
|
|
78
95
|
# A failed pull must not be followed by "everything is up to date". Timeouts
|
package/package.json
CHANGED