unoverse 0.1.100 → 0.1.102

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.
@@ -242,10 +242,22 @@ data "aws_ami" "ubuntu" {
242
242
  }
243
243
  }
244
244
 
245
+ # The key pair, from the operator's own public key. AWS accepts an imported key, so nothing
246
+ # has to pre-exist in the account and no private key is ever downloaded or stored.
247
+ resource "aws_key_pair" "operator" {
248
+ count = var.operator_public_key != "" ? 1 : 0
249
+ key_name = "${var.name}-operator"
250
+ public_key = var.operator_public_key
251
+ }
252
+
253
+ locals {
254
+ key_name = var.operator_public_key != "" ? aws_key_pair.operator[0].key_name : var.ssh_key_name
255
+ }
256
+
245
257
  resource "aws_instance" "app" {
246
258
  ami = data.aws_ami.ubuntu.id
247
259
  instance_type = local.s.instance
248
- key_name = var.ssh_key_name
260
+ key_name = local.key_name
249
261
  vpc_security_group_ids = [aws_security_group.app.id]
250
262
 
251
263
  root_block_device {
@@ -102,3 +102,13 @@ variable "marketplace_url" {
102
102
  type = string
103
103
  default = ""
104
104
  }
105
+
106
+ # YOUR public key, uploaded as this universe's EC2 key pair. Deploying means ssh-ing from
107
+ # the machine that runs `unoverse deploy`, so the key that must work is the one that machine
108
+ # holds — not whichever pair happens to exist in the account. Empty falls back to
109
+ # ssh_key_name, for an operator with no local key.
110
+ variable "operator_public_key" {
111
+ description = "An ssh public key (the contents of ~/.ssh/id_ed25519.pub). Empty = use ssh_key_name instead."
112
+ type = string
113
+ default = ""
114
+ }
@@ -366,6 +366,35 @@ _ensure_ground_config() {
366
366
  esac
367
367
  fi
368
368
 
369
+ # CAN THIS MACHINE ACTUALLY REACH WHAT IT IS ABOUT TO BUILD?
370
+ #
371
+ # An EC2 key pair the operator does not hold applies perfectly and then fails at the ship
372
+ # step: eleven minutes of RDS and ElastiCache provisioning, then "Permission denied
373
+ # (publickey)". Generating the ground correctly is not enough — tfvars is the developer's
374
+ # file and `unoverse update` never rewrites it, so a ground made before this check keeps
375
+ # its unusable key forever and fails the same way every time.
376
+ #
377
+ # Checked here, before the plan, because the cost of being wrong is money and eleven
378
+ # minutes, and the fix is one line in a file this command already edits.
379
+ if [ "$cloud" = "aws" ]; then
380
+ local named_key pub_key
381
+ named_key=$(grep -E '^ssh_key_name[[:space:]]*=' "$tfv" 2>/dev/null | sed -E 's/.*"([^"]*)".*/\1/')
382
+ if [ -n "$named_key" ] && ! grep -qE '^operator_public_key[[:space:]]*=[[:space:]]*"ssh-' "$tfv" 2>/dev/null; then
383
+ for pub_key in "$HOME/.ssh/id_ed25519.pub" "$HOME/.ssh/id_rsa.pub" ""; do
384
+ [ -n "$pub_key" ] && [ -f "$pub_key" ] && break
385
+ done
386
+ if [ -n "$pub_key" ]; then
387
+ node -e 'const fs=require("fs");const[f,v]=process.argv.slice(1);let s=fs.readFileSync(f,"utf8");
388
+ s=s.replace(/^ssh_key_name\s*=.*$/m,"ssh_key_name = \"\" # empty = terraform uploads operator_public_key below\noperator_public_key = "+JSON.stringify(v)+" # your key: the deploy ssh-es from this machine");
389
+ fs.writeFileSync(f,s)' "$tfv" "$(cat "$pub_key")"
390
+ ok "SSH key: your own ${DIM}($(basename "$pub_key") — replacing '"'"'$named_key'"'"', whose private half is not on this machine)${NC}"
391
+ else
392
+ warn "No ssh key on this machine. The deploy will build, then fail to reach the server"
393
+ info "Make one with ${BOLD}ssh-keygen -t ed25519${NC} and run this again"
394
+ fi
395
+ fi
396
+ fi
397
+
369
398
  _tf_fill docr_token DOCR_TOKEN "Registry access token (from your Unoverse admin)"
370
399
  _tf_fill openai_api_key OPENAI_API_KEY "OPENAI_API_KEY (powers the platform's AI)"
371
400
  if grep -q '^auth_issuer[[:space:]]*=[[:space:]]*"FILL_ME"' "$tfv" 2>/dev/null; then
@@ -540,9 +569,8 @@ _ground_apply() {
540
569
  }
541
570
 
542
571
  cmd_deploy() {
543
- # A LEADING GROUND NAME IS NOT A SUBCOMMAND. `unoverse deploy aws` names the cloud;
544
- # `unoverse deploy init` names the step; `unoverse deploy aws init` does both. Take the
545
- # ground off the front when it is there, and leave everything else exactly as it was.
572
+ # A LEADING GROUND NAME IS NOT A SUBCOMMAND. `unoverse deploy aws` names the cloud. Take
573
+ # it off the front when it is there, and leave everything else exactly as it was.
546
574
  local GROUND_ARG=""
547
575
  case "${1:-}" in
548
576
  do|digitalocean|aws|amazon) GROUND_ARG="$1"; shift ;;
@@ -700,9 +728,8 @@ EOF
700
728
  # A FIRST DEPLOY TO A NEW SERVER IS A PROVISION, NOT AN IMAGE PULL. Bare `deploy` meant
701
729
  # only "pull the latest images and restart", which on a freshly built droplet fails on
702
730
  # "Destination directory /opt/gravity does not exist" — /opt/gravity being the directory
703
- # install.yml creates. The remedy was to know that `unoverse deploy init` exists, which
704
- # is exactly the kind of knowledge this CLI is supposed to remove: deploy owns the whole
705
- # journey, so it asks the server what it is and picks the right playbook itself.
731
+ # install.yml creates. Deploy owns the whole journey, so it asks the server what it is and
732
+ # picks the right playbook itself rather than expecting anyone to know a second command.
706
733
  # THE MARKER IS "SETUP FINISHED", NOT "A DIRECTORY EXISTS". Testing for /opt/gravity
707
734
  # looked right and was not: install.yml creates that directory as its seventh task, so a
708
735
  # first-time setup that then FAILED at the database step still left it behind. The next
@@ -714,7 +741,7 @@ EOF
714
741
  "$deploy_user@$deploy_host" 'test -f /opt/gravity/.setup-complete' >/dev/null 2>&1; then
715
742
  echo ""
716
743
  info "This server's setup has not finished. Running it ${DIM}(install, database, verify)${NC}"
717
- subcommand="init"
744
+ subcommand="first-time"
718
745
  fi
719
746
  fi
720
747
 
@@ -732,10 +759,10 @@ EOF
732
759
  -e "universe_root=$ROOT" \
733
760
  -e "env_file=$env_prod"
734
761
  ;;
735
- init|full)
736
- # FIRST-TIME setup, END TO END: install db verify. One command after
737
- # `terraform apply`. Hardening is a deliberate follow-up choice, never a
738
- # default (POCs get verified first; harden when you decide to keep it).
762
+ first-time)
763
+ # INTERNAL, never typed. Deploy reaches this by finding no .setup-complete stamp on
764
+ # the server: install database verify, end to end. Hardening stays a deliberate
765
+ # follow-up (POCs get verified first; harden when you decide to keep the box).
739
766
  info "First-time setup: install → database → verify"
740
767
  echo ""
741
768
  info "[1/3] Provisioning (Docker, services, mounts)..."
@@ -743,7 +770,7 @@ EOF
743
770
  -i "$tmp_inventory" \
744
771
  "$ansible_dir/playbooks/install.yml" \
745
772
  -e "universe_root=$ROOT" \
746
- -e "env_file=$env_prod" || { rm -f "$tmp_inventory"; fail "install failed fix and re-run: unoverse deploy init"; exit 1; }
773
+ -e "env_file=$env_prod" || { rm -f "$tmp_inventory"; fail "install failed. Fix the error above, then: unoverse deploy $cloud"; exit 1; }
747
774
  echo ""
748
775
  info "[2/3] Database setup..."
749
776
  ansible-playbook \
@@ -751,14 +778,14 @@ EOF
751
778
  "$ansible_dir/playbooks/db-setup.yml" \
752
779
  -e "universe_root=$ROOT" \
753
780
  -e "pg_admin_url=$pg_admin_url" \
754
- -e "env_file=$env_prod" || { rm -f "$tmp_inventory"; fail "db setup failed fix and re-run: unoverse deploy db"; exit 1; }
781
+ -e "env_file=$env_prod" || { rm -f "$tmp_inventory"; fail "database setup failed. Fix the error above, then: unoverse deploy $cloud"; exit 1; }
755
782
  echo ""
756
783
  info "[3/3] Verifying..."
757
784
  ansible-playbook \
758
785
  -i "$tmp_inventory" \
759
786
  "$ansible_dir/playbooks/test-connectivity.yml" \
760
787
  -e "universe_root=$ROOT" \
761
- -e "env_file=$env_prod" || { rm -f "$tmp_inventory"; fail "verification failed inspect and re-run: unoverse deploy test"; exit 1; }
788
+ -e "env_file=$env_prod" || { rm -f "$tmp_inventory"; fail "verification failed. Look at the output above, then: unoverse deploy $cloud"; exit 1; }
762
789
  # Only now. Every step passed, so the next deploy can safely be an image push.
763
790
  ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=15 \
764
791
  "$deploy_user@$deploy_host" 'touch /opt/gravity/.setup-complete' >/dev/null 2>&1
@@ -239,9 +239,28 @@ _ground_aws() {
239
239
  region=$(aws configure get region 2>/dev/null)
240
240
  [ -n "$region" ] && ok "region: $region" || { region="us-east-1"; info "no default region configured. Using us-east-1"; }
241
241
 
242
+ # A KEY PAIR YOU CANNOT USE IS NOT A KEY PAIR. This took the FIRST key pair in the region,
243
+ # which on a real account is whatever was created years ago in the console — its private
244
+ # half is not on this laptop, so terraform applied happily and Ansible then died on
245
+ # "Permission denied (publickey)" after eleven minutes of RDS provisioning.
246
+ #
247
+ # The operator's own key is the one that works, because deploying IS ssh-ing from here.
248
+ # Upload it under the universe's name and use that: AWS lets terraform import a public
249
+ # key, so nothing has to pre-exist and nothing has to be downloaded. Fall back to the
250
+ # account's existing pairs only when this machine has no key at all.
251
+ local pubkey=""
252
+ for pubkey in "$HOME/.ssh/id_ed25519.pub" "$HOME/.ssh/id_rsa.pub" ""; do
253
+ [ -n "$pubkey" ] && [ -f "$pubkey" ] && break
254
+ done
255
+
242
256
  keys=$(aws ec2 describe-key-pairs --query 'KeyPairs[].KeyName' --output text 2>/dev/null | tr '\t' '\n')
243
257
  first_key=$(echo "$keys" | head -1)
244
- if [ -n "$first_key" ]; then
258
+ if [ -n "$pubkey" ]; then
259
+ first_key="" # terraform creates the pair from operator_public_key
260
+ ok "SSH key: your own ${DIM}($(basename "$pubkey") — uploaded as ${GROUND_NAME:-this universe}-operator)${NC}"
261
+ elif [ -n "$first_key" ]; then
262
+ warn "no SSH key on this machine — using the account's ${BOLD}$first_key${NC}"
263
+ warn "the deploy will fail unless you hold its private half"
245
264
  ok "EC2 key pair: $first_key"
246
265
  else
247
266
  warn "no EC2 key pairs in $region. Create one first (aws ec2 create-key-pair)"
@@ -268,7 +287,8 @@ _ground_aws() {
268
287
  region = "$region"
269
288
  name = "$GROUND_NAME"
270
289
  admin_cidr = "${ip:-FILL_ME}${ip:+/32}" # YOUR IP — the only SSH source
271
- ssh_key_name = "${first_key:-FILL_ME}" # must already exist in the region
290
+ ssh_key_name = "${first_key}" # empty = terraform uploads operator_public_key below
291
+ operator_public_key = "$( [ -n "$pubkey" ] && cat "$pubkey" )" # your key: the deploy ssh-es from this machine
272
292
  admin_email = "FILL_ME" # initial admin (Cognito user, all roles; invite emailed)
273
293
  size = "small" # small (POC) | medium | large
274
294
  # Domain is OPTIONAL — empty brings the universe up on the ALB's DNS name over
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unoverse",
3
- "version": "0.1.100",
3
+ "version": "0.1.102",
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",