10.7 KB362 lines
Blame
1#!/bin/bash
2# Grove cloud-init — bootstraps a Grove instance on any VPS.
3# Sets up Mononoke + Grove Bridge + Grove API + Grove Web + Grove Collab + Caddy.
4#
5# Placeholders replaced by the deploy page before use:
6# __INSTANCE_ID__ — hub instance record ID
7# __HUB_DOMAIN__ — hub domain (e.g., grove.host)
8# __INSTANCE_DOMAIN__ — instance domain (optional, empty for IP-only)
9# __HUB_TOKEN__ — user's JWT for the readiness callback
10
11set -euo pipefail
12
13GROVE_DIR="/opt/grove"
14DATA_DIR="/data/grove"
15HUB_DOMAIN="__HUB_DOMAIN__"
16INSTANCE_DOMAIN="__INSTANCE_DOMAIN__"
17INSTANCE_ID="__INSTANCE_ID__"
18HUB_TOKEN="__HUB_TOKEN__"
19
20# ── Install Docker if missing ────────────────────────────────────────
21
22if ! command -v docker &>/dev/null; then
23 echo "Installing Docker..."
24 curl -fsSL https://get.docker.com | sh
25fi
26
27# ── Discover public IP (provider-agnostic) ───────────────────────────
28
29DROPLET_IP=$(curl -sf https://ifconfig.me || curl -sf https://api.ipify.org || hostname -I | awk '{print $1}')
30echo "Public IP: ${DROPLET_IP}"
31
32# ── Generate secrets ─────────────────────────────────────────────────
33
34JWT_SECRET=$(openssl rand -hex 32)
35
36# ── Generate self-signed TLS certs for Mononoke SLAPI ───────────────
37
38mkdir -p "${DATA_DIR}/tls"
39
40# CA
41openssl genrsa -out "${DATA_DIR}/tls/ca.key" 4096 2>/dev/null
42openssl req -new -x509 -key "${DATA_DIR}/tls/ca.key" \
43 -out "${DATA_DIR}/tls/ca.crt" -days 3650 \
44 -subj "/CN=Grove Instance CA" 2>/dev/null
45
46# Server cert
47openssl genrsa -out "${DATA_DIR}/tls/server.key" 4096 2>/dev/null
48openssl req -new -key "${DATA_DIR}/tls/server.key" \
49 -out "${DATA_DIR}/tls/server.csr" \
50 -subj "/CN=${INSTANCE_DOMAIN:-${DROPLET_IP}}" 2>/dev/null
51
52cat > "${DATA_DIR}/tls/ext.cnf" <<EOF
53authorityKeyIdentifier=keyid,issuer
54basicConstraints=CA:FALSE
55keyUsage=digitalSignature,keyEncipherment
56extendedKeyUsage=serverAuth,clientAuth
57subjectAltName=@alt_names
58
59[alt_names]
60IP.1 = ${DROPLET_IP}
61DNS.1 = ${INSTANCE_DOMAIN:-localhost}
62EOF
63
64openssl x509 -req -in "${DATA_DIR}/tls/server.csr" \
65 -CA "${DATA_DIR}/tls/ca.crt" -CAkey "${DATA_DIR}/tls/ca.key" \
66 -CAcreateserial -out "${DATA_DIR}/tls/server.crt" \
67 -days 3650 -extfile "${DATA_DIR}/tls/ext.cnf" 2>/dev/null
68
69rm -f "${DATA_DIR}/tls/server.csr" "${DATA_DIR}/tls/ext.cnf" "${DATA_DIR}/tls/ca.srl"
70
71echo "TLS certificates generated."
72
73# ── Create directory structure ───────────────────────────────────────
74
75mkdir -p "${DATA_DIR}/mononoke-config/common"
76mkdir -p "${DATA_DIR}/mononoke-config/repo_definitions"
77mkdir -p "${DATA_DIR}/mononoke-config/repos"
78mkdir -p "${DATA_DIR}/configerator"
79mkdir -p "${DATA_DIR}/mononoke"
80mkdir -p "${DATA_DIR}/api"
81mkdir -p "${GROVE_DIR}"
82
83# ── Write Mononoke config ───────────────────────────────────────────
84
85cat > "${DATA_DIR}/mononoke-config/common/common.toml" <<'TOML'
86enable_http_control_api = true
87
88[internal_identity]
89identity_type = "SERVICE"
90identity_data = "grove"
91TOML
92
93cat > "${DATA_DIR}/mononoke-config/common/storage.toml" <<'TOML'
94[default]
95
96[default.metadata]
97[default.metadata.local]
98local_db_path = "/data/grove/mononoke/metadata"
99
100[default.blobstore]
101[default.blobstore.blob_files]
102path = "/data/grove/mononoke/blobs"
103
104[default.mutable_blobstore]
105[default.mutable_blobstore.blob_files]
106path = "/data/grove/mononoke/mutable_blobs"
107TOML
108
109# ── Write justknobs.json (required by Mononoke) ─────────────────────
110
111cat > "${DATA_DIR}/justknobs.json" <<'JSON'
112{}
113JSON
114
115# ── Write .env ───────────────────────────────────────────────────────
116
117cat > "${GROVE_DIR}/.env" <<EOF
118JWT_SECRET=${JWT_SECRET}
119DOMAIN=${INSTANCE_DOMAIN:-${DROPLET_IP}}
120HUB_DOMAIN=${HUB_DOMAIN}
121EOF
122
123# ── Write Caddyfile ──────────────────────────────────────────────────
124
125if [ -n "${INSTANCE_DOMAIN}" ]; then
126 CADDY_ADDR="${INSTANCE_DOMAIN}"
127else
128 CADDY_ADDR=":80"
129fi
130
131cat > "${GROVE_DIR}/Caddyfile" <<CADDYEOF
132${CADDY_ADDR} {
133 handle /api/auth/* {
134 reverse_proxy https://${HUB_DOMAIN} {
135 header_up Host ${HUB_DOMAIN}
136 }
137 }
138
139 handle /api/instances/* {
140 reverse_proxy https://${HUB_DOMAIN} {
141 header_up Host ${HUB_DOMAIN}
142 }
143 }
144
145 handle /v2/* {
146 reverse_proxy registry:5000
147 }
148
149 handle /collab/socket.io/* {
150 uri strip_prefix /collab
151 reverse_proxy grove-web:3334
152 }
153
154 handle /api/repos {
155 reverse_proxy grove-api:4000
156 }
157
158 handle /api/repos/* {
159 reverse_proxy grove-api:4000
160 }
161
162 handle {
163 reverse_proxy grove-web:3000
164 }
165
166 header {
167 X-Content-Type-Options nosniff
168 X-Frame-Options DENY
169 }
170}
171CADDYEOF
172
173# ── Write docker-compose.yml ────────────────────────────────────────
174
175cat > "${GROVE_DIR}/docker-compose.yml" <<'COMPOSE'
176services:
177 registry:
178 image: registry:2
179 ports:
180 - "127.0.0.1:5000:5000"
181 volumes:
182 - registry-data:/var/lib/registry
183 environment:
184 - REGISTRY_STORAGE_DELETE_ENABLED=true
185 restart: unless-stopped
186
187 mononoke-slapi:
188 image: ${HUB_DOMAIN}/grove-mononoke:latest
189 command:
190 - --listening-host-port
191 - "0.0.0.0:8443"
192 - --config-path
193 - /data/grove/mononoke-config
194 - --local-configerator-path
195 - /data/grove/configerator
196 - --cache-mode
197 - disabled
198 - --just-knobs-config-path
199 - /data/grove/justknobs.json
200 - --tls-certificate
201 - /data/grove/tls/server.crt
202 - --tls-private-key
203 - /data/grove/tls/server.key
204 - --tls-ca
205 - /data/grove/tls/ca.crt
206 ports:
207 - "8443:8443"
208 volumes:
209 - /data/grove:/data/grove
210 restart: unless-stopped
211
212 mononoke-git:
213 image: ${HUB_DOMAIN}/grove-mononoke:latest
214 entrypoint: ["/usr/local/bin/git_server"]
215 command:
216 - --listen-host
217 - "0.0.0.0"
218 - --listen-port
219 - "8080"
220 - --config-path
221 - /data/grove/mononoke-config
222 - --local-configerator-path
223 - /data/grove/configerator
224 - --cache-mode
225 - disabled
226 - --just-knobs-config-path
227 - /data/grove/justknobs.json
228 - --tls-certificate
229 - /data/grove/tls/server.crt
230 - --tls-private-key
231 - /data/grove/tls/server.key
232 - --tls-ca
233 - /data/grove/tls/ca.crt
234 ports:
235 - "8080:8080"
236 volumes:
237 - /data/grove:/data/grove
238 depends_on:
239 mononoke-slapi:
240 condition: service_started
241 restart: unless-stopped
242
243 grove-bridge:
244 image: ${HUB_DOMAIN}/grove-mononoke:latest
245 entrypoint: ["/usr/local/bin/grove_bridge"]
246 command:
247 - --listening-host-port
248 - "0.0.0.0:3100"
249 - --config-path
250 - /data/grove/mononoke-config
251 - --local-configerator-path
252 - /data/grove/configerator
253 - --cache-mode
254 - disabled
255 - --just-knobs-config-path
256 - /data/grove/justknobs.json
257 volumes:
258 - /data/grove:/data/grove
259 depends_on:
260 mononoke-slapi:
261 condition: service_started
262 restart: unless-stopped
263
264 grove-api:
265 image: ${HUB_DOMAIN}/grove-api:latest
266 environment:
267 - PORT=4000
268 - DATABASE_PATH=/data/grove.db
269 - JWT_SECRET=${JWT_SECRET}
270 - GROVE_BRIDGE_URL=http://grove-bridge:3100
271 - MONONOKE_CONFIG_PATH=/data/grove/mononoke-config
272 - CANOPY_ENABLED=true
273 - CANOPY_WORKSPACE_DIR=/canopy/workspaces
274 - NODE_ENV=production
275 volumes:
276 - /data/grove/api:/data
277 - /var/run/docker.sock:/var/run/docker.sock
278 - /data/grove:/data/grove
279 - canopy-workspaces:/canopy/workspaces
280 depends_on:
281 grove-bridge:
282 condition: service_started
283 restart: unless-stopped
284
285 grove-web:
286 image: ${HUB_DOMAIN}/grove-web:latest
287 volumes:
288 - collab-data:/data/collab
289 environment:
290 - GROVE_API_URL=http://grove-api:4000
291 - DATA_DIR=/data/collab
292 - COLLAB_SOCKET_PORT=3334
293 - JWT_SECRET=${JWT_SECRET}
294 - NODE_ENV=production
295 depends_on:
296 - grove-api
297 restart: unless-stopped
298
299 caddy:
300 image: caddy:2-alpine
301 ports:
302 - "80:80"
303 - "443:443"
304 volumes:
305 - ./Caddyfile:/etc/caddy/Caddyfile:ro
306 - caddy-data:/data
307 - caddy-config:/config
308 depends_on:
309 - grove-web
310 - grove-api
311 restart: unless-stopped
312
313volumes:
314 registry-data:
315 canopy-workspaces:
316 collab-data:
317 caddy-data:
318 caddy-config:
319COMPOSE
320
321# ── Pull images from hub registry ───────────────────────────────────
322
323echo "Pulling images from ${HUB_DOMAIN}..."
324docker pull "${HUB_DOMAIN}/grove-mononoke:latest"
325docker pull "${HUB_DOMAIN}/grove-api:latest"
326docker pull "${HUB_DOMAIN}/grove-web:latest"
327
328# ── Start ────────────────────────────────────────────────────────────
329
330cd "${GROVE_DIR}"
331docker compose up -d
332
333# ── Wait for health ──────────────────────────────────────────────────
334
335echo "Waiting for Grove Bridge to be healthy..."
336for i in $(seq 1 60); do
337 if curl -sf http://localhost:3100/health > /dev/null 2>&1; then
338 echo "Grove Bridge is healthy."
339 break
340 fi
341 sleep 5
342done
343
344# ── Notify hub that instance is ready ────────────────────────────────
345
346curl -sf -X POST "https://${HUB_DOMAIN}/api/instances/${INSTANCE_ID}/ready" \
347 -H "Content-Type: application/json" \
348 -d "{\"ip\": \"${DROPLET_IP}\", \"jwt_secret\": \"${JWT_SECRET}\"}" || true
349
350# ── Done ─────────────────────────────────────────────────────────────
351
352touch "${GROVE_DIR}/.setup-complete"
353echo ""
354echo "================================================"
355echo " Grove instance deployment complete."
356echo " IP: ${DROPLET_IP}"
357echo " URL: ${INSTANCE_DOMAIN:+https://${INSTANCE_DOMAIN}}${INSTANCE_DOMAIN:-http://${DROPLET_IP}}"
358echo ""
359echo " Mononoke SLAPI: :8443 (TLS)"
360echo " CA cert: ${DATA_DIR}/tls/ca.crt"
361echo "================================================"
362