unoverse 0.1.77 → 0.1.78
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,13 +1,14 @@
|
|
|
1
|
+
# NO DEFAULTS ON EITHER OF THESE — see the same note in infra/digitalocean/variables.tf.
|
|
2
|
+
# Where it runs and what it is called are the developer's answers, and a default quietly
|
|
3
|
+
# supplies mine instead.
|
|
1
4
|
variable "region" {
|
|
2
5
|
description = "AWS region. Must have Bedrock model access enabled for the models you use."
|
|
3
6
|
type = string
|
|
4
|
-
default = "us-east-1"
|
|
5
7
|
}
|
|
6
8
|
|
|
7
9
|
variable "name" {
|
|
8
|
-
description = "Prefix for every resource (also the Cognito domain prefix, so keep it dns-safe)."
|
|
10
|
+
description = "Prefix for every resource (also the Cognito domain prefix, so keep it dns-safe), and the resource group they are tagged into."
|
|
9
11
|
type = string
|
|
10
|
-
default = "universe-poc"
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
variable "admin_cidr" {
|
|
@@ -5,16 +5,18 @@ variable "do_token" {
|
|
|
5
5
|
sensitive = true
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
# NO DEFAULTS ON EITHER OF THESE. They used to default to "lon1" and "universe-poc", which
|
|
9
|
+
# meant a tfvars missing them built somebody's universe in London under a placeholder name
|
|
10
|
+
# and reported success. Where it runs and what it is called are the developer's answers, so
|
|
11
|
+
# terraform should refuse to plan without them rather than quietly supply mine.
|
|
8
12
|
variable "region" {
|
|
9
|
-
description = "DigitalOcean region for everything."
|
|
13
|
+
description = "DigitalOcean region for everything (doctl compute region list)."
|
|
10
14
|
type = string
|
|
11
|
-
default = "lon1"
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
variable "name" {
|
|
15
|
-
description = "Prefix for every resource."
|
|
18
|
+
description = "Prefix for every resource, and the name of the DigitalOcean project they are grouped under."
|
|
16
19
|
type = string
|
|
17
|
-
default = "universe-poc"
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
variable "size" {
|
package/operator/lib/deploy.sh
CHANGED
|
@@ -125,6 +125,24 @@ _ensure_ground_config() {
|
|
|
125
125
|
fi
|
|
126
126
|
}
|
|
127
127
|
echo ""
|
|
128
|
+
|
|
129
|
+
# YOUR IP MOVED. admin_cidr is captured once, when the ground is generated, and it is
|
|
130
|
+
# the ONLY address the firewall lets near port 22. Home broadband, a phone hotspot or a
|
|
131
|
+
# different office and the address is stale — at which point terraform applies happily,
|
|
132
|
+
# every resource is correct, and the ship step dies on "connect to host ... port 22:
|
|
133
|
+
# Operation timed out" with nothing on screen connecting the two. Check it every deploy
|
|
134
|
+
# and just fix it: the firewall is ours, and a one-rule update is not a decision.
|
|
135
|
+
local cur_ip cur_cidr have_cidr
|
|
136
|
+
cur_ip=$(curl -s --max-time 5 https://api.ipify.org 2>/dev/null || curl -s --max-time 5 https://ifconfig.me 2>/dev/null)
|
|
137
|
+
have_cidr=$(grep -E '^admin_cidr[[:space:]]*=' "$tfv" 2>/dev/null | sed -E 's/.*"([^"]+)".*/\1/')
|
|
138
|
+
if [ -n "$cur_ip" ] && [ -n "$have_cidr" ] && [ "$have_cidr" != "FILL_ME" ]; then
|
|
139
|
+
cur_cidr="$cur_ip/32"
|
|
140
|
+
if [ "$have_cidr" != "$cur_cidr" ]; then
|
|
141
|
+
node -e 'const fs=require("fs");const[f,v]=process.argv.slice(1);let s=fs.readFileSync(f,"utf8");s=s.replace(/^admin_cidr(\s*)=\s*"[^"]*"/m,"admin_cidr$1= "+JSON.stringify(v));fs.writeFileSync(f,s)' "$tfv" "$cur_cidr"
|
|
142
|
+
ok "Your address changed ${DIM}($have_cidr → $cur_cidr). Admin access follows you${NC}"
|
|
143
|
+
fi
|
|
144
|
+
fi
|
|
145
|
+
|
|
128
146
|
# POSTGRES: ASK, DO NOT ASSUME. The ground discovers an existing cluster and writes
|
|
129
147
|
# it as a COMMENTED line, so the default silently provisions a SECOND database next
|
|
130
148
|
# to one the account already pays for. A choice with a monthly bill attached is a
|
|
@@ -244,25 +262,34 @@ _ground_apply() {
|
|
|
244
262
|
*) fail "Terraform could not plan the change:"; tail -20 "$tmp/plan.log" | sed 's/^/ /'; rm -rf "$tmp"; return 1 ;;
|
|
245
263
|
esac
|
|
246
264
|
|
|
247
|
-
|
|
265
|
+
# STOPPING IS FOR LOSING SOMETHING, NOT FOR BUILDING IT. Typing `unoverse deploy` is
|
|
266
|
+
# already the decision; a y/N gate straight afterwards asks the same question twice and
|
|
267
|
+
# makes the developer's own instruction feel like a risk. The summary above still shows
|
|
268
|
+
# exactly what appears and what it costs — it is a briefing, not a checkpoint. tfsummary
|
|
269
|
+
# exits 3 when the plan destroys or replaces something, and THAT is worth interrupting.
|
|
270
|
+
local summary_rc=0
|
|
271
|
+
terraform -chdir="$dir" show -json "$planfile" 2>/dev/null | node "$GRAVITY_LIB/tfsummary.mjs"
|
|
272
|
+
summary_rc=${PIPESTATUS[1]}
|
|
273
|
+
if [ "$summary_rc" = "2" ]; then
|
|
274
|
+
# The plan could not be read, so nothing above is trustworthy. Show the real thing and
|
|
275
|
+
# fall back to asking: silence plus an unreadable plan is not consent.
|
|
248
276
|
terraform -chdir="$dir" show "$planfile"
|
|
277
|
+
summary_rc=3
|
|
249
278
|
fi
|
|
250
279
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
info "Nothing was created. Run ${BOLD}unoverse deploy${NC} when you are ready"
|
|
265
|
-
rm -rf "$tmp"; return 1
|
|
280
|
+
local docpage
|
|
281
|
+
[ "$cloud" = "aws" ] && docpage="aws" || docpage="digitalocean"
|
|
282
|
+
echo -e " ${DIM}How it fits together: https://github.com/unoverse-platform/docs/blob/main/architecture/$docpage.md${NC}"
|
|
283
|
+
echo ""
|
|
284
|
+
|
|
285
|
+
if [ "$summary_rc" = "3" ]; then
|
|
286
|
+
local REPLY
|
|
287
|
+
echo -e " ${RED}This removes or rebuilds things that already exist.${NC}"
|
|
288
|
+
read -r -p " Type y to go ahead, or Enter to stop: " REPLY
|
|
289
|
+
if [[ ! "$REPLY" =~ ^[Yy]$ ]]; then
|
|
290
|
+
info "Nothing was changed. Run ${BOLD}unoverse deploy${NC} when you are ready"
|
|
291
|
+
rm -rf "$tmp"; return 1
|
|
292
|
+
fi
|
|
266
293
|
fi
|
|
267
294
|
|
|
268
295
|
echo ""
|
package/operator/lib/ground.sh
CHANGED
|
@@ -13,6 +13,44 @@ _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
|
+
# THE UNIVERSE HAS A NAME, so ask for one. This file used to write name = "universe-poc"
|
|
17
|
+
# as a literal, so every universe on every account carried the same placeholder — it names
|
|
18
|
+
# the droplet, the cache, the load balancer and the cloud project, and the developer first
|
|
19
|
+
# meets it in their provider dashboard, after the bill has started. A generated file is not
|
|
20
|
+
# a question: nobody edits it before running deploy.
|
|
21
|
+
#
|
|
22
|
+
# Sets GROUND_NAME (and GROUND_REGION when a default is passed). Both are sanitised to what
|
|
23
|
+
# cloud resource names allow, so a typed "My POC" cannot fail an apply five minutes in.
|
|
24
|
+
_ground_identity() {
|
|
25
|
+
local valid="$1" suggest
|
|
26
|
+
suggest=$(basename "$ROOT" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9-]+/-/g; s/^-+//; s/-+$//')
|
|
27
|
+
suggest="${suggest:-universe}"
|
|
28
|
+
echo ""
|
|
29
|
+
read -r -p " Name this universe [$suggest]: " GROUND_NAME
|
|
30
|
+
GROUND_NAME=$(echo "${GROUND_NAME:-$suggest}" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9-]+/-/g; s/^-+//; s/-+$//')
|
|
31
|
+
GROUND_NAME="${GROUND_NAME:-$suggest}"
|
|
32
|
+
|
|
33
|
+
# NO DEFAULT REGION. A default is a choice made on the developer's behalf, and the
|
|
34
|
+
# previous one was mine: `lon1`, written straight into the file, which is how a universe
|
|
35
|
+
# meant for Singapore was built in London. There is no neutral region to fall back to, so
|
|
36
|
+
# this asks until it gets a real answer and checks it against the provider's own list —
|
|
37
|
+
# a typo here surfaces as a five-minute apply that fails on the last resource.
|
|
38
|
+
# "-" means the region is not this ground's to ask: AWS reads it from the developer's own
|
|
39
|
+
# CLI configuration, which is already their answer.
|
|
40
|
+
GROUND_REGION=""
|
|
41
|
+
[ "$valid" = "-" ] && return 0
|
|
42
|
+
while [ -z "$GROUND_REGION" ]; do
|
|
43
|
+
read -r -p " Which region? " GROUND_REGION
|
|
44
|
+
GROUND_REGION=$(echo "$GROUND_REGION" | tr -d '[:space:]')
|
|
45
|
+
# Validate only against a list we actually retrieved. An empty list means the lookup
|
|
46
|
+
# failed, not that every region is wrong.
|
|
47
|
+
if [ -n "$GROUND_REGION" ] && [ -n "$valid" ] && ! echo "$valid" | tr ' ' '\n' | grep -qx "$GROUND_REGION"; then
|
|
48
|
+
warn "$GROUND_REGION is not a region in your account. Pick one from the list above"
|
|
49
|
+
GROUND_REGION=""
|
|
50
|
+
fi
|
|
51
|
+
done
|
|
52
|
+
}
|
|
53
|
+
|
|
16
54
|
_ground_do() {
|
|
17
55
|
local dir="$ROOT/infra/digitalocean"
|
|
18
56
|
local out="$dir/terraform.tfvars"
|
|
@@ -88,13 +126,21 @@ _ground_do() {
|
|
|
88
126
|
info "no existing Postgres cluster. Terraform will provision one"
|
|
89
127
|
fi
|
|
90
128
|
|
|
129
|
+
local regions
|
|
130
|
+
regions=$(doctl compute region list --format Slug --no-header 2>/dev/null | tr -d '\r' | xargs)
|
|
131
|
+
if [ -n "$regions" ]; then
|
|
132
|
+
echo ""
|
|
133
|
+
echo -e " ${DIM}Regions available to you: $regions${NC}"
|
|
134
|
+
fi
|
|
135
|
+
_ground_identity "$regions"
|
|
136
|
+
|
|
91
137
|
cat > "$out" <<EOF
|
|
92
138
|
# Generated by \`unoverse ground do\` on $(date +%Y-%m-%d). Discovered values are
|
|
93
139
|
# prefilled; every FILL_ME needs YOUR value before \`terraform apply\`.
|
|
94
140
|
# Token: set DIGITALOCEAN_TOKEN in the environment (preferred) or do_token here.
|
|
95
141
|
|
|
96
|
-
region = "
|
|
97
|
-
name = "
|
|
142
|
+
region = "$GROUND_REGION" # doctl compute region list
|
|
143
|
+
name = "$GROUND_NAME"
|
|
98
144
|
size = "small" # small (POC) | medium | large
|
|
99
145
|
admin_cidr = "${ip:-FILL_ME}${ip:+/32}" # YOUR IP — SSH + Dozzle only
|
|
100
146
|
ssh_key_name = "${first_key:-FILL_ME}" # must already exist in the DO account
|
|
@@ -185,12 +231,15 @@ _ground_aws() {
|
|
|
185
231
|
info "multiple Route53 zones. Pick one in the file (listed there)"
|
|
186
232
|
fi
|
|
187
233
|
|
|
234
|
+
# Region already came from the AWS CLI's own configuration, so only the name is asked.
|
|
235
|
+
_ground_identity "-"
|
|
236
|
+
|
|
188
237
|
cat > "$out" <<EOF
|
|
189
238
|
# Generated by \`unoverse ground aws\` on $(date +%Y-%m-%d). Discovered values are
|
|
190
239
|
# prefilled; every FILL_ME needs YOUR value before \`terraform apply\`.
|
|
191
240
|
|
|
192
241
|
region = "$region"
|
|
193
|
-
name = "
|
|
242
|
+
name = "$GROUND_NAME"
|
|
194
243
|
admin_cidr = "${ip:-FILL_ME}${ip:+/32}" # YOUR IP — the only SSH source
|
|
195
244
|
ssh_key_name = "${first_key:-FILL_ME}" # must already exist in the region
|
|
196
245
|
admin_email = "FILL_ME" # initial admin (Cognito user, all roles; invite emailed)
|
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
* their size, counts the plumbing (database users, firewall rules, generated passwords)
|
|
12
12
|
* rather than listing it, and shouts about anything being destroyed.
|
|
13
13
|
*
|
|
14
|
+
* EXIT CODE IS THE ANSWER TO "should the caller stop and ask?". 0 when the plan only adds
|
|
15
|
+
* or adjusts — deploy just builds, because typing `unoverse deploy` already said to. 3
|
|
16
|
+
* when something is destroyed or replaced, which is the only case worth interrupting a
|
|
17
|
+
* developer for. 2 when the JSON could not be read, so the caller shows raw terraform.
|
|
18
|
+
*
|
|
14
19
|
* NOT A COST ORACLE. The developer sees an order of magnitude rather than a surprise. Rates
|
|
15
20
|
* move, so the total is labelled approximate and the real bill is the provider's.
|
|
16
21
|
*
|
|
@@ -138,7 +143,7 @@ process.stdin.on("end", () => {
|
|
|
138
143
|
const out = [];
|
|
139
144
|
if (total === 0) {
|
|
140
145
|
console.log(`\n ${green("●")} Your infrastructure already matches. Nothing to change.\n`);
|
|
141
|
-
|
|
146
|
+
return;
|
|
142
147
|
}
|
|
143
148
|
|
|
144
149
|
const width = Math.max(...Object.values(groups).flat().map((d) => d.what.length), 0) + 2;
|
|
@@ -169,4 +174,11 @@ process.stdin.on("end", () => {
|
|
|
169
174
|
}
|
|
170
175
|
|
|
171
176
|
console.log("\n" + out.join("\n"));
|
|
177
|
+
|
|
178
|
+
// Tell the caller whether this is worth stopping for. Losing something is; building
|
|
179
|
+
// something is not. `process.exitCode` rather than `process.exit`, which can cut off
|
|
180
|
+
// the output above while it is still draining down a pipe.
|
|
181
|
+
const destructive =
|
|
182
|
+
groups.delete.length + groups.replace.length + supporting.delete + supporting.replace;
|
|
183
|
+
if (destructive) process.exitCode = 3;
|
|
172
184
|
});
|
package/package.json
CHANGED