1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat >&2 <<'EOF'
usage: scripts/dev-droplet.sh create [name] | setup [name] | push <publish-dir> [name] | swc [name] -- <socialweb-computer args>
scripts/dev-droplet.sh ssh [name] [cmd...] | list | down
scripts/dev-droplet.sh sync [name] [ref] [work] | build [name] [work]
EOF
exit 2
}
sizes="${DEV_DROPLET_SIZES:-c-60-intel c2-60vcpu-120gb-intel c-48-intel c2-48vcpu-96gb-intel c-48 c2-48vcpu-96gb c-32-intel c-32 c-16-intel c-16}"
regions="${DEV_DROPLET_REGIONS:-sfo3 nyc3 atl1 tor1 nyc2 sfo2 nyc1 lon1 fra1 ams3}"
image="${DEV_DROPLET_IMAGE:-ubuntu-24-04-x64}"
key_file="${DEV_DROPLET_KEY:-$HOME/.ssh/id_ed25519}"
[ -f "$key_file" ] || key_file="$HOME/.ssh/id_ed25519_no_password"
state="$HOME/.tmp/dev-droplet"
guest_image="${SOCIALWEB_IT_GUEST_IMAGE_DIR:-/var/lib/socialweb-computer/guest-image}"
mkdir -p "$state"
say() { echo "dev-droplet: $*" >&2; }
ip_of() { cat "$state/${1:-dev-work-0001}.ip" 2>/dev/null || { say "no droplet ${1:-dev-work-0001}; run create"; exit 1; }; }
rssh() { local n="$1"; shift; ssh -i "$key_file" -o StrictHostKeyChecking=accept-new -o ServerAliveInterval=30 "root@$(ip_of "$n")" "$@"; }
dssh() { local n="$1"; shift; ssh -i "$key_file" -o StrictHostKeyChecking=accept-new -o ServerAliveInterval=30 "dev@$(ip_of "$n")" "$@"; }
cmd="${1:-}"
[ -n "$cmd" ] || usage
shift
case "$cmd" in
create)
name="${1:-dev-work-0001}"
[ "$(doctl compute droplet list --tag-name dev-work --no-header --format ID | grep -c .)" -lt 8 ] || { say "eight dev-work droplets exist already"; exit 1; }
key_id="$(doctl compute ssh-key list --no-header --format ID,FingerPrint | awk -v fp="$(ssh-keygen -E md5 -lf "$key_file.pub" | sed 's/^[0-9]* MD5://; s/ .*//')" '$2 == fp { print $1 }')"
[ -n "$key_id" ] || { say "key $key_file.pub is not registered with doctl"; exit 1; }
for size in $sizes; do
for region in $regions; do
if out="$(doctl compute droplet create "$name" --size "$size" --image "$image" --region "$region" --tag-name dev-work --ssh-keys "$key_id" --wait --no-header --format ID 2>&1)"; then
ip=""
until [ -n "$ip" ]; do ip="$(doctl compute droplet get "$out" --no-header --format PublicIPv4)"; sleep 2; done
echo "$ip" >"$state/$name.ip"
say "$name created: $size in $region at $ip"
break 2
fi
say "$size in $region: ${out##*: }"
done
done
[ -f "$state/$name.ip" ] || exit 1
until rssh "$name" true 2>/dev/null; do sleep 3; done
;;
setup)
name="${1:-dev-work-0001}"
repo="$(cd "$(dirname "$0")/.." && pwd)"
go_ver="$(awk '/^go /{print $2}' "$repo/go.mod")"
deno_ver="$(jq -r '.assets.deno.version' "$repo/internal/versions/versions.json")"
rssh "$name" env GO_VER="$go_ver" DENO_VER="$deno_ver" bash -s <<'EOF'
set -euxo pipefail
export DEBIAN_FRONTEND=noninteractive
apt_opts="-o DPkg::Lock::Timeout=600"
apt-get $apt_opts update -qq
apt-get $apt_opts install -y -qq uidmap rsync zstd acl jq curl unzip xz-utils dbus-user-session git build-essential gnupg binutils passt podman iproute2 util-linux
modprobe tun
echo tun >/etc/modules-load.d/tun.conf
printf 'kernel.apparmor_restrict_unprivileged_userns=0\nuser.max_user_namespaces=1000000\n' >/etc/sysctl.d/60-swc.conf
sysctl --system >/dev/null
id dev >/dev/null 2>&1 || useradd -m -s /bin/bash dev
id test-0001 >/dev/null 2>&1 || useradd -m -s /bin/bash -G kvm test-0001
sed -i '/^test-0001:/d;/^dev:/d' /etc/subuid /etc/subgid
echo 'dev:100000:65536' >>/etc/subuid
echo 'dev:100000:65536' >>/etc/subgid
install -d -m 700 -o dev -g dev /home/dev/.ssh
install -m 600 -o dev -g dev /root/.ssh/authorized_keys /home/dev/.ssh/authorized_keys
echo 'test-0001:655360:1048576' >>/etc/subuid
echo 'test-0001:655360:1048576' >>/etc/subgid
mkdir -p /etc/systemd/system/user@.service.d
printf '[Service]\nDelegate=cpu memory pids\n' >/etc/systemd/system/user@.service.d/swc-delegate.conf
systemctl daemon-reload
loginctl enable-linger test-0001
loginctl enable-linger dev
sleep 2
systemctl restart "user@$(id -u test-0001).service"
mkdir -p /etc/rancher/node
test -s /etc/rancher/node/password || head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \n' >/etc/rancher/node/password
chown test-0001:test-0001 /etc/rancher/node/password
chmod 600 /etc/rancher/node/password
mkdir -p /var/lib/socialweb-computer /var/tmp/swcit
chmod 1777 /var/tmp/swcit
chown dev:dev /var/lib/socialweb-computer
test -c /dev/kvm
test -x /usr/local/go/bin/go && /usr/local/go/bin/go version | grep -q "go$GO_VER " || { curl -fsSL "https://go.dev/dl/go$GO_VER.linux-amd64.tar.gz" | tar -C /usr/local -xzf - --transform "s,^go,go.new," && rm -rf /usr/local/go && mv /usr/local/go.new /usr/local/go; }
ln -sf /usr/local/go/bin/go /usr/local/go/bin/gofmt /usr/local/bin/
test -x /usr/local/bin/deno && deno --version | grep -q "deno ${DENO_VER#v} " || { curl -fsSL -o /var/tmp/deno.zip "https://github.com/denoland/deno/releases/download/$DENO_VER/deno-x86_64-unknown-linux-gnu.zip" && unzip -oq /var/tmp/deno.zip -d /usr/local/bin && rm /var/tmp/deno.zip; }
runuser -u dev -- bash -c 'mkdir -p ~/.deno/bin && ln -sf /usr/local/bin/deno ~/.deno/bin/deno && { test -x ~/.cargo/bin/rustup || curl -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain none; }'
grep -q CONTAINER /home/dev/.profile || echo 'export PATH=$HOME/.cargo/bin:$PATH CONTAINER=podman' >>/home/dev/.profile
EOF
;;
push)
publish="${1:?publish dir}"
name="${2:-dev-work-0001}"
ssh_e="ssh -i $key_file -o StrictHostKeyChecking=accept-new"
rsync -a --delete -e "$ssh_e" "$publish/" "root@$(ip_of "$name"):$publish/"
rsync -a -z -e "$ssh_e" "$guest_image/" "root@$(ip_of "$name"):/var/lib/socialweb-computer/guest-image/"
;;
swc)
name="dev-work-0001"
[ "${1:-}" = "--" ] || { name="$1"; shift; }
shift
publish="${SOCIALWEB_PUBLISH_DIR:?set SOCIALWEB_PUBLISH_DIR to the pushed publish dir}"
uid="$(rssh "$name" id -u test-0001)"
rssh "$name" cd /var/tmp '&&' runuser -u test-0001 -- env HOME=/home/test-0001 XDG_RUNTIME_DIR=/run/user/$uid DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$uid/bus PATH=/usr/local/bin:/usr/bin:/bin LANG=C.UTF-8 "$publish/bin/socialweb-computer" $(printf '%q ' "$@")
;;
sync)
name="${1:-dev-work-0001}"
ref="${2:-origin/main}"
work="${3:-swc-dev}"
dssh "$name" "bash -lc $(printf %q "cd ~; [ -d socialweb-computer-kcp ] || git clone -q https://github.com/publicdomainrelay/socialweb-computer-kcp; cd socialweb-computer-kcp && git fetch -q origin && { [ -d ~/w/$work ] && git -C ~/w/$work checkout -qf --detach $ref || git worktree add -qf --detach ~/w/$work $ref; } && git -C ~/w/$work log --oneline -1")"
;;
build)
name="${1:-dev-work-0001}"
work="${2:-swc-dev}"
dssh "$name" "bash -lc $(printf %q "cd ~/w/$work && scripts/build-socialweb-computer.sh --dev --publish /var/tmp/$work")"
;;
ssh)
name="${1:-dev-work-0001}"
[ $# -eq 0 ] || shift
rssh "$name" "$@"
;;
list)
doctl compute droplet list --tag-name dev-work --format ID,Name,PublicIPv4,Memory,VCPUs,Region,Status
;;
down)
doctl compute droplet delete --tag-name dev-work --force
rm -f "$state"/*.ip
doctl compute droplet list --tag-name dev-work --no-header --format ID | grep -c . | grep -qx 0 && say "no dev-work droplet is left"
;;
*)
usage
;;
esac