-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathmakefile
More file actions
319 lines (297 loc) · 15.3 KB
/
Copy pathmakefile
File metadata and controls
319 lines (297 loc) · 15.3 KB
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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
.ONESHELL:
.SILENT:
UV_PROJECT := ./client
PROJECT_ABS := $(abspath $(UV_PROJECT))
# One dev cluster per checkout. Several run side by side on one machine, so
# every daemon-global or host-global name they need (docker network, container
# labels, published head port, published node ports) derives from this name.
# Named after the git worktree root, not CURDIR: in the monorepo this
# directory is always "burla", so CURDIR would give every worktree's cluster
# the same identity.
BURLA_CLUSTER_NAME ?= $(shell basename "$$(git rev-parse --show-toplevel 2>/dev/null || pwd)" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g')
BURLA_CLUSTER_NETWORK := burla-$(BURLA_CLUSTER_NAME)
# Hashed rather than allocated so a checkout's dashboard URL and node ports stay
# the same across restarts. Override either if two checkouts ever collide.
BURLA_HEAD_PORT ?= $(shell python3 -c 'import hashlib,sys;print(5100+int(hashlib.sha256(sys.argv[1].encode()).hexdigest(),16)%800)' '$(BURLA_CLUSTER_NAME)')
BURLA_NODE_PORT_BASE ?= $(shell python3 -c 'import hashlib,sys;print(9000+(int(hashlib.sha256(sys.argv[1].encode()).hexdigest(),16)%250)*20)' '$(BURLA_CLUSTER_NAME)')
BURLA_DASHBOARD_URL := http://localhost:$(BURLA_HEAD_PORT)
# local-dev's node base image, built here from the Dockerfile rather than
# pulled, so local-dev needs no image registry at all. Node code is
# bind-mounted at runtime, so it only needs rebuilding when the node Dockerfile
# or node_service's locked dependencies change. (The head has no image: it runs
# as a host subprocess straight from this checkout, like remote-dev.)
LOCAL_NODE_IMAGE := burla-node-service:local-dev
# Stamped onto the image as a label so `local-dev` can tell an image built from
# today's Dockerfile from one built from an older one. Without it a stale image
# under the same tag is silently reused and its nodes die on whatever the
# Dockerfile added since (a pre-DinD image exits 127 on `dnsmasq`).
NODE_IMAGE_FINGERPRINT := $(shell python3 -c 'import hashlib;print(hashlib.sha256(open("node_service/Dockerfile","rb").read()).hexdigest()[:16])')
NODE_IMAGE_FINGERPRINT_LABEL := burla.node-image-fingerprint
# A test shell on a chosen interpreter. `--python` is passed per-run instead of
# pinned, so switching versions never edits the tracked `client/.python-version`.
# Each version gets its own venv so switching back and forth (or running two at
# once) doesn't rebuild the default `.venv` every time.
define TEST_SHELL
set -e
uv python install $(1) >/dev/null 2>&1
$(MAKE) -C main_service ensure-frontend
UV_PROJECT_ENVIRONMENT=$(PROJECT_ABS)/.venv-$(1) \
uv run --project $(PROJECT_ABS) --group dev --python $(1) \
python -m burla._test_shell
endef
.PHONY: 3.11-dev 3.12-dev 3.13-dev 3.14-dev local-dev dev-up dev-down \
dev-clean remote-dev local-images image-seed stop-all cluster-info \
node-logs test test-service test-e2e test-dashboard kill-kernels
3.11-dev:
$(call TEST_SHELL,3.11)
3.12-dev:
$(call TEST_SHELL,3.12)
3.13-dev:
$(call TEST_SHELL,3.13)
3.14-dev:
$(call TEST_SHELL,3.14)
# Every tier needs a cluster running for THIS checkout: `make local-dev` (or
# `make remote-dev`) in another terminal. Tests reach it at
# BURLA_CLUSTER_DASHBOARD_URL, defaulted here to this checkout's head port so
# they never talk to another checkout's cluster.
#
# DISABLE_BURLA_TELEMETRY silences the client's telemetry (which the backend
# forwards to Slack) for the pytest process and every rpm subprocess it
# spawns. It does NOT reach the already-running head/nodes/workers: those
# inherited their env from the `make local-dev` that started them, so export
# DISABLE_BURLA_TELEMETRY=True before `make local-dev` to silence those too.
test:
DISABLE_BURLA_TELEMETRY=True \
BURLA_CLUSTER_DASHBOARD_URL=$${BURLA_CLUSTER_DASHBOARD_URL:-$(BURLA_DASHBOARD_URL)} \
BURLA_REQUIRE_CLUSTER=1 uv run --project ./client --group dev pytest -s --disable-warnings
# Service-level tests. Requires a cluster for this checkout.
test-service:
DISABLE_BURLA_TELEMETRY=True \
BURLA_CLUSTER_DASHBOARD_URL=$${BURLA_CLUSTER_DASHBOARD_URL:-$(BURLA_DASHBOARD_URL)} \
BURLA_REQUIRE_CLUSTER=1 uv run --project ./client --group dev pytest -m service -s --disable-warnings
# End-to-end tests. Requires a cluster for this checkout.
test-e2e:
DISABLE_BURLA_TELEMETRY=True \
BURLA_CLUSTER_DASHBOARD_URL=$${BURLA_CLUSTER_DASHBOARD_URL:-$(BURLA_DASHBOARD_URL)} \
BURLA_REQUIRE_CLUSTER=1 uv run --project ./client --group dev pytest -m e2e -s --disable-warnings
# Dashboard-UI tests, driven through real Chromium. Requires a cluster.
test-dashboard:
uv run --project ./client --group dev playwright install chromium; \
DISABLE_BURLA_TELEMETRY=True \
BURLA_CLUSTER_DASHBOARD_URL=$${BURLA_CLUSTER_DASHBOARD_URL:-$(BURLA_DASHBOARD_URL)} \
BURLA_REQUIRE_CLUSTER=1 uv run --project ./client --group dev pytest -m dashboard -s --disable-warnings
cluster-info:
echo "cluster: $(BURLA_CLUSTER_NAME)"; \
echo "dashboard: $(BURLA_DASHBOARD_URL)"; \
echo "docker network: $(BURLA_CLUSTER_NETWORK)"; \
echo "node ports: $(BURLA_NODE_PORT_BASE)+"
# Node logs as the head recorded them, oldest last. Unlike `docker logs node_*`
# these cover remote-dev's EC2 nodes and survive the container being replaced
# between jobs. Usage: make node-logs [NODE=<id substring>] [JOB=<job id>] [N=<lines>]
node-logs:
BURLA_ENVIRONMENT=$${BURLA_ENVIRONMENT:-test} \
uv run --project $(PROJECT_ABS) --group dev python -m burla._node_logs \
--node "$(NODE)" --job "$(JOB)" --lines "$${N:-200}" \
--namespace "$(BURLA_CLUSTER_NAME)" \
--local-dev-db "$(PWD)/_local_dev_state/history.db"
# Emergency machine-wide cleanup only. Normal dev clusters clean up their own
# nodes; this removes every local cluster's containers and caches.
stop-all:
set -e; \
pkill -f 'python -m burla._local_dev' 2>/dev/null || true; \
ids=$$(docker ps -aq --filter label=burla-cluster); \
if [ -n "$$ids" ]; then docker rm -f -v $$ids >/dev/null; fi; \
vols=$$(docker volume ls -q --filter label=burla-cluster); \
if [ -n "$$vols" ]; then docker volume rm $$vols >/dev/null; fi; \
docker volume rm burla-uv-cache >/dev/null 2>&1 || true; \
echo "Removed every burla dev cluster on this machine."
# Rebuild local-dev's node base image. `local-dev` does this for you when it's
# missing or stale; run it by hand to pick up new node_service dependencies,
# which the fingerprint can't see (they're installed from a git clone, not from
# this checkout, so an unchanged Dockerfile can still produce a new image).
local-images:
set -e; \
docker build -t $(LOCAL_NODE_IMAGE) \
--label $(NODE_IMAGE_FINGERPRINT_LABEL)=$(NODE_IMAGE_FINGERPRINT) \
./node_service; \
echo "Built $(LOCAL_NODE_IMAGE)."
# Each node runs its own docker daemon, so without this tarball every node
# would download the default worker image from the registry at boot. Kept
# current with the registry (when online) because nodes always `docker pull`,
# and a stale seed would make that pull a full re-download instead of a no-op.
# The `.ref` sidecar names the image the tarball holds (for the node, which
# skips loading an image it already has) and its id (for the check below).
image-seed:
set -e; \
mkdir -p _image_seed; \
docker pull -q python:3.12 >/dev/null 2>&1 || true; \
docker image inspect python:3.12 >/dev/null 2>&1 || docker pull python:3.12; \
current=$$(docker image inspect -f '{{.Id}}' python:3.12); \
saved=$$(awk '{print $$2}' _image_seed/python-3.12.ref 2>/dev/null || true); \
if [ "$$current" != "$$saved" ]; then \
docker save python:3.12 -o _image_seed/python-3.12.tar; \
echo "python:3.12 $$current" > _image_seed/python-3.12.ref; \
echo "Saved python:3.12 seed for node-local docker daemons."; \
fi
# The whole cluster runs on this machine: the head as a host subprocess
# hot-reloading this checkout (like remote-dev), nodes as privileged "fake VM"
# containers on this checkout's own network, each running its own docker
# daemon with its workers inside it (exactly the prod topology). Uses
# `LOCAL_DEV_CONFIG` in `main_service.__init__.py` (1 node by default; raise
# with LOCAL_DEV_NODE_QUANTITY). Needs a working AWS identity + saved cluster
# token: nodes authorize callers against the backend's user list for this
# cluster id, so a bogus id makes every node fail to boot.
#
# `_local_dev_state` (the head's SQLite history) survives restarts on purpose:
# past jobs' dashboard pages keep working after the head is killed and brought
# back. Rehydrated nodes/jobs whose processes died with the old head are
# retired by the head's node/job reapers a few minutes after startup, exactly
# as in prod. `make dev-clean` wipes the state when a fresh start is wanted.
local-dev:
set -e; \
if nc -z localhost $(BURLA_HEAD_PORT) 2>/dev/null; then \
echo ""; \
echo "ERROR: something is already listening on port $(BURLA_HEAD_PORT), probably this"; \
echo " checkout's cluster. Starting another would wipe its live state"; \
echo " (_local_dev_state) out from under it."; \
echo ""; \
echo " Fix: stop the existing cluster in its terminal, then re-run make local-dev"; \
echo ""; \
exit 1; \
fi; \
NODE_IMAGE=$${BURLA_NODE_IMAGE:-$(LOCAL_NODE_IMAGE)}; \
if [ "$${NODE_IMAGE}" = "$(LOCAL_NODE_IMAGE)" ]; then \
built_from=$$(docker image inspect \
-f '{{index .Config.Labels "$(NODE_IMAGE_FINGERPRINT_LABEL)"}}' \
$${NODE_IMAGE} 2>/dev/null || true); \
if [ "$${built_from}" != "$(NODE_IMAGE_FINGERPRINT)" ]; then \
$(MAKE) local-images; \
fi; \
fi; \
$(MAKE) image-seed; \
$(MAKE) -C main_service ensure-frontend; \
BACKEND_URL=$${BURLA_BACKEND_URL:-https://test.backend.burla.dev}; \
AWS_ACCOUNT=$$(aws sts get-caller-identity --query Account --output text 2>/dev/null || true); \
if [ -z "$${AWS_ACCOUNT}" ] && [ -z "$${BURLA_DEV_PROJECT}" ]; then \
echo ""; \
echo "ERROR: no usable AWS identity, so this cluster has no real cluster id"; \
echo " and its nodes cannot authorize themselves (they would fail to"; \
echo " boot with a 401)."; \
echo ""; \
echo " Fix: aws sso login"; \
echo ""; \
exit 1; \
fi; \
PROJECT_ID=$${BURLA_DEV_PROJECT:-aws-$${AWS_ACCOUNT}}; \
TOKEN_FILE=$${XDG_DATA_HOME:-$$HOME/.local/share}/burla-test/clusters/$${PROJECT_ID}/cluster_token; \
[ -f "$$TOKEN_FILE" ] || TOKEN_FILE=$$HOME/Library/Application\ Support/burla-test/clusters/$${PROJECT_ID}/cluster_token; \
CLUSTER_ID_TOKEN=$${BURLA_CLUSTER_ID_TOKEN:-$$(cat "$$TOKEN_FILE" 2>/dev/null || true)}; \
if [ -z "$${CLUSTER_ID_TOKEN}" ]; then \
echo ""; \
echo "ERROR: no cluster token saved for [$${PROJECT_ID}], so this cluster's"; \
echo " nodes cannot authorize themselves (they would fail to boot with"; \
echo " a 401)."; \
echo ""; \
echo " Fix: burla login (registers this cluster and saves its token)"; \
echo ""; \
exit 1; \
fi; \
echo "Starting cluster [$(BURLA_CLUSTER_NAME)] at $(BURLA_DASHBOARD_URL) (cluster id $${PROJECT_ID})"; \
ids=$$(docker ps -aq --filter label=burla-cluster=$(BURLA_CLUSTER_NAME)); \
if [ -n "$$ids" ]; then docker rm -f $$ids >/dev/null; fi; \
for scratch in _shared_workspace _node_auth; do \
rm -rf ./$$scratch; mkdir -p ./$$scratch; chmod 777 ./$$scratch; \
done; \
mkdir -p ./_local_dev_state; chmod 777 ./_local_dev_state; \
docker network create $(BURLA_CLUSTER_NETWORK) 2>/dev/null || true; \
BURLA_ENVIRONMENT=test \
PROJECT_ID=$${PROJECT_ID} \
IN_LOCAL_DEV_MODE=True \
CLOUD_PROVIDER=$${BURLA_CLOUD:-aws} \
CLUSTER_ID_TOKEN=$${CLUSTER_ID_TOKEN} \
BURLA_CLUSTER_NAME=$(BURLA_CLUSTER_NAME) \
LOCAL_DEV_NETWORK=$(BURLA_CLUSTER_NETWORK) \
LOCAL_DEV_HEAD_HOST=host.docker.internal \
LOCAL_DEV_NODE_PORT_BASE=$(BURLA_NODE_PORT_BASE) \
LOCAL_DEV_NODE_QUANTITY=$${LOCAL_DEV_NODE_QUANTITY:-1} \
BURLA_NODE_IMAGE=$${NODE_IMAGE} \
BURLA_BACKEND_URL=$${BACKEND_URL} \
REDIRECT_LOCALLY_ON_LOGIN=True \
HOST_PWD=$(PWD) \
PORT=$(BURLA_HEAD_PORT) \
HISTORY_DB_PATH=$(PWD)/_local_dev_state/history.db \
uv run --project $(PROJECT_ABS) --group dev python -m burla._local_dev
# Detached twin of `local-dev`: the cluster runs in its own process session,
# owned by the OS instead of the terminal (or agent session) that started it,
# so it stays up until `make dev-down`. Logs stream to
# _local_dev_state/head.log. The 10m startup budget covers a first-run node
# image build.
dev-up:
@set -e; \
if nc -z localhost $(BURLA_HEAD_PORT) 2>/dev/null; then \
echo "Head [$(BURLA_CLUSTER_NAME)] is already up at $(BURLA_DASHBOARD_URL)"; \
exit 0; \
fi; \
mkdir -p _local_dev_state; \
python3 -c 'import subprocess; p = subprocess.Popen(["make", "local-dev"], stdout=open("_local_dev_state/head.log", "w"), stderr=subprocess.STDOUT, start_new_session=True); open("_local_dev_state/head.pid", "w").write(str(p.pid))'; \
echo "Starting head [$(BURLA_CLUSTER_NAME)] detached (log: _local_dev_state/head.log)"; \
for i in $$(seq 1 120); do \
if nc -z localhost $(BURLA_HEAD_PORT) 2>/dev/null; then \
echo "Head [$(BURLA_CLUSTER_NAME)] is up at $(BURLA_DASHBOARD_URL)"; \
echo "Boot nodes: BURLA_CLUSTER_DASHBOARD_URL=$(BURLA_DASHBOARD_URL) BURLA_ENVIRONMENT=test uv run --project ./client burla cluster start"; \
exit 0; \
fi; \
if ! kill -0 $$(cat _local_dev_state/head.pid) 2>/dev/null; then \
echo "Head process exited during startup; last log lines:"; \
tail -20 _local_dev_state/head.log; \
exit 1; \
fi; \
sleep 5; \
done; \
echo "Timed out waiting for the head; see _local_dev_state/head.log"; \
exit 1
# Stop the detached cluster: SIGINT its whole session (same as ctrl-C on a
# foreground `make local-dev`) so the head deletes its own nodes, then
# force-remove any container left carrying this cluster's label. History in
# _local_dev_state is kept; `make dev-up` serves it again.
dev-down:
@pid=$$(cat _local_dev_state/head.pid 2>/dev/null || true); \
if [ -n "$$pid" ] && kill -0 $$pid 2>/dev/null; then \
kill -INT -- -$$pid 2>/dev/null || kill -INT $$pid; \
for i in $$(seq 1 30); do \
kill -0 $$pid 2>/dev/null || break; \
sleep 1; \
done; \
kill -0 $$pid 2>/dev/null && { kill -KILL -- -$$pid 2>/dev/null || true; }; \
echo "Cluster [$(BURLA_CLUSTER_NAME)] stopped."; \
else \
echo "No detached cluster process for [$(BURLA_CLUSTER_NAME)]."; \
fi; \
rm -f _local_dev_state/head.pid; \
ids=$$(docker ps -aq --filter label=burla-cluster=$(BURLA_CLUSTER_NAME)); \
if [ -n "$$ids" ]; then docker rm -f $$ids >/dev/null; fi
# Wipe this worktree's persisted local-dev state (job history, head log).
# Refuses while the cluster is up: the head would keep writing to a deleted db.
dev-clean:
@if nc -z localhost $(BURLA_HEAD_PORT) 2>/dev/null; then \
echo "Cluster [$(BURLA_CLUSTER_NAME)] is running; make dev-down first."; \
exit 1; \
fi; \
rm -rf _local_dev_state; \
echo "Cleared _local_dev_state for [$(BURLA_CLUSTER_NAME)]."
# `main_service` runs here as a local subprocess hot-reloading this checkout;
# nodes are real cloud VMs: EC2 in the Burla test AWS account by default, or
# Azure VMs with BURLA_CLOUD=azure (uses your active `az` subscription).
# Nodes reach this head through the relay, so many of these run at once on
# one machine. Node VMs download this working tree from the head at boot,
# uncommitted edits included, so nothing needs committing or pushing.
remote-dev:
set -e; \
$(MAKE) -C main_service ensure-frontend; \
BURLA_ENVIRONMENT=test \
BURLA_CLOUD=$${BURLA_CLOUD:-aws} \
BURLA_CLUSTER_NAME=$(BURLA_CLUSTER_NAME) \
BURLA_HEAD_PORT=$(BURLA_HEAD_PORT) \
uv run --project $(PROJECT_ABS) --group dev python -m burla._remote_dev
kill-kernels:
pkill -f ipykernel