返回 CodeWhale
provision.sh
1 #!/usr/bin/env bash
2 # EXPERIMENTAL — DigitalOcean smoke-lab provisioning for the CodeWhale
3 # remote workbench (issue #1990 "clearly documented better alternative"
4 # clause). Creates ONE Ubuntu 24.04 droplet plus a cloud firewall that
5 # allows inbound SSH only. Prints the monthly price from the DO API and
6 # requires a typed "yes" before creating anything billable.
7 #
8 # Auth: doctl must be authenticated. Either
9 # doctl auth init # paste token interactively, or
10 # export DIGITALOCEAN_ACCESS_TOKEN=... # doctl reads this env var
11 #
12 # Usage:
13 # bash scripts/remote-smoke/digitalocean/provision.sh
14 #
15 # Tunables (env):
16 # DROPLET_NAME default codewhale-smoke
17 # DO_REGION default sfo3 (San Francisco)
18 # DROPLET_SIZE default s-1vcpu-2gb (~$12/mo; prebuilt binaries mean no
19 # Rust build, so 1 vCPU / 2 GB is enough for the smoke.
20 # Use s-2vcpu-2gb/s-2vcpu-4gb for a longer-lived host.)
21 # DROPLET_IMAGE default ubuntu-24-04-x64
22 # SSH_PUBKEY default ~/.ssh/id_ed25519.pub (imported if not present)
23 # RESTRICT_SSH_TO_MY_IP default true (firewall source = caller IP /32)
24 set -euo pipefail
25
26 DROPLET_NAME="${DROPLET_NAME:-codewhale-smoke}"
27 DO_REGION="${DO_REGION:-sfo3}"
28 DROPLET_SIZE="${DROPLET_SIZE:-s-1vcpu-2gb}"
29 DROPLET_IMAGE="${DROPLET_IMAGE:-ubuntu-24-04-x64}"
30 SSH_PUBKEY="${SSH_PUBKEY:-$HOME/.ssh/id_ed25519.pub}"
31 SSH_KEY_NAME="${SSH_KEY_NAME:-${DROPLET_NAME}-key}"
32 FIREWALL_NAME="${FIREWALL_NAME:-${DROPLET_NAME}-ssh-only}"
33 RESTRICT_SSH_TO_MY_IP="${RESTRICT_SSH_TO_MY_IP:-true}"
34
35 command -v doctl >/dev/null || { echo "doctl is required (brew install doctl)" >&2; exit 1; }
36 doctl account get >/dev/null || { echo "doctl is not authenticated; run 'doctl auth init' or set DIGITALOCEAN_ACCESS_TOKEN" >&2; exit 1; }
37 [[ -f "$SSH_PUBKEY" ]] || { echo "SSH public key not found: $SSH_PUBKEY" >&2; exit 1; }
38
39 echo "== Preflight =="
40 [[ "$(doctl compute region list --format Slug,Available --no-header | awk -v r="$DO_REGION" '$1 == r {print $2}')" == "true" ]] \
41 || { echo "Region ${DO_REGION} not available" >&2; exit 1; }
42
43 read -r PRICE VCPUS MEM DISK < <(doctl compute size list \
44 --format Slug,PriceMonthly,VCPUs,Memory,Disk --no-header \
45 | awk -v s="$DROPLET_SIZE" '$1 == s {print $2, $3, $4, $5}')
46 [[ -n "${PRICE:-}" ]] || { echo "Size ${DROPLET_SIZE} not found" >&2; exit 1; }
47
48 echo "Region: $DO_REGION"
49 echo "Droplet: $DROPLET_NAME"
50 echo "Image: $DROPLET_IMAGE"
51 echo "Size: $DROPLET_SIZE (${VCPUS} vCPU / ${MEM} MB RAM / ${DISK} GB disk)"
52 echo "Monthly price: \$$PRICE USD (billed hourly until destroyed)"
53 echo "SSH key: $SSH_PUBKEY -> '$SSH_KEY_NAME'"
54 echo "Firewall: $FIREWALL_NAME (inbound 22/tcp only)"
55 echo
56 read -r -p "Create this droplet and start billing? Type 'yes' to proceed: " CONFIRM
57 [[ "$CONFIRM" == "yes" ]] || { echo "Aborted; nothing created."; exit 1; }
58
59 echo "== Import SSH key =="
60 KEY_ID=$(doctl compute ssh-key list --format ID,Name --no-header | awk -v n="$SSH_KEY_NAME" '$2 == n {print $1; exit}')
61 if [[ -z "$KEY_ID" ]]; then
62 KEY_ID=$(doctl compute ssh-key import "$SSH_KEY_NAME" --public-key-file "$SSH_PUBKEY" --format ID --no-header)
63 echo "imported $SSH_KEY_NAME (id $KEY_ID)"
64 else
65 echo "key $SSH_KEY_NAME already exists (id $KEY_ID); reusing"
66 fi
67
68 echo "== Create droplet =="
69 doctl compute droplet create "$DROPLET_NAME" \
70 --region "$DO_REGION" \
71 --image "$DROPLET_IMAGE" \
72 --size "$DROPLET_SIZE" \
73 --ssh-keys "$KEY_ID" \
74 --tag-name codewhale-smoke \
75 --wait >/dev/null
76 DROPLET_ID=$(doctl compute droplet list --format ID,Name --no-header | awk -v n="$DROPLET_NAME" '$2 == n {print $1; exit}')
77 IP=$(doctl compute droplet get "$DROPLET_ID" --format PublicIPv4 --no-header)
78 echo "created $DROPLET_NAME (id $DROPLET_ID, $IP)"
79
80 echo "== Cloud firewall: SSH only =="
81 SRC="0.0.0.0/0,address:::/0"
82 if [[ "$RESTRICT_SSH_TO_MY_IP" == "true" ]]; then
83 MYIP=$(curl -fsS https://api.ipify.org)
84 SRC="${MYIP}/32"
85 fi
86 if ! doctl compute firewall list --format Name --no-header | grep -qx "$FIREWALL_NAME"; then
87 doctl compute firewall create \
88 --name "$FIREWALL_NAME" \
89 --inbound-rules "protocol:tcp,ports:22,address:${SRC}" \
90 --outbound-rules "protocol:tcp,ports:all,address:0.0.0.0/0,address:::/0 protocol:udp,ports:all,address:0.0.0.0/0,address:::/0 protocol:icmp,address:0.0.0.0/0,address:::/0" \
91 --droplet-ids "$DROPLET_ID" >/dev/null
92 echo "firewall $FIREWALL_NAME created: inbound 22/tcp from ${SRC}, all else blocked"
93 else
94 FW_ID=$(doctl compute firewall list --format ID,Name --no-header | awk -v n="$FIREWALL_NAME" '$2 == n {print $1; exit}')
95 doctl compute firewall add-droplets "$FW_ID" --droplet-ids "$DROPLET_ID"
96 echo "existing firewall $FIREWALL_NAME attached"
97 fi
98
99 echo
100 echo "== Done =="
101 echo "Droplet: $DROPLET_NAME ($DO_REGION)"
102 echo "Public IP: $IP"
103 echo "SSH: ssh -i ${SSH_PUBKEY%.pub} root@${IP}"
104 echo " (DO Ubuntu images log in as root, not ubuntu)"
105 echo
106 echo "Teardown when finished (stops billing):"
107 echo " bash scripts/remote-smoke/digitalocean/teardown.sh"
108
108 lines BASH