-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathTiltfile
More file actions
342 lines (323 loc) · 16.4 KB
/
Copy pathTiltfile
File metadata and controls
342 lines (323 loc) · 16.4 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
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# MIT Learn Stack — Local Development Entry Point
# Run: tilt up
# Docs: local-dev/README.md
# ---------------------------------------------------------------------------
# Developer configuration
# ---------------------------------------------------------------------------
config.define_string_list("enabled_apps", usage="Apps to run: mit-learn learn-ai mitxonline odl-video-service")
config.define_bool("per_app_databases", usage="Deploy isolated DB/Valkey per app namespace")
# openedx_mode is declared but not yet wired: nothing branches on it, and
# local-dev/apps/openedx/Tiltfile is not yet included by the APPS loop below.
config.define_string("openedx_mode", usage="qa (default) or local (Tutor)")
config.define_string_list("prebuilt_tags", usage="Prebuilt image tag overrides per app, e.g. mit-learn=0.62.0 learn-ai=0.28.3")
config.define_string("disk_keep_tags", usage="Newest tilt-built image tags kept per repo by the disk janitor (default: 3). Overrides LOCAL_DEV_DISK_KEEP_TAGS env var.")
config.define_string("disk_buildcache_max_gb", usage="Docker build-cache size cap in GB (default: 10% of total disk; 0 disables). Overrides LOCAL_DEV_BUILDCACHE_MAX_GB env var.")
config.define_string("log_retention_period", usage="How long Grafana/Loki keeps local-dev logs, as a whole number of days, e.g. 72h or 3d (default: 168h). Overrides LOCAL_DEV_LOG_RETENTION env var.")
cfg = config.parse()
enabled_apps = cfg.get("enabled_apps", ["mit-learn", "learn-ai", "mitxonline", "odl-video-service"])
per_app_databases = cfg.get("per_app_databases", False)
openedx_mode = cfg.get("openedx_mode", "qa")
# Every service hostname, CORS origin, APISIX route, and Keycloak redirect URI
# derives from this value. local-dev/tiltlib.star reads the same environment
# variable when applying app manifests.
root_domain = os.environ.get("LOCAL_DEV_ROOT_DOMAIN", "mit.dev")
# The TLS certificate covers only this domain, so an unresolvable hostname means
# nothing the stack serves is reachable. DNS and /etc/hosts both satisfy this.
_probe_host = "sso.ol." + root_domain
if str(local(
"python3 -c 'import socket,sys; socket.getaddrinfo(sys.argv[1], None)' %s >/dev/null 2>&1 && echo ok || echo missing" % _probe_host,
quiet=True, echo_off=True,
)).strip() != "ok":
fail(
("%s does not resolve, so nothing in the stack will be reachable.\n" +
" LOCAL_DEV_ROOT_DOMAIN is currently '%s'.\n" +
" Fix by either:\n" +
" - running ./local-dev/scripts/setup.sh (adds 127.0.0.1 entries to /etc/hosts), or\n" +
" - pointing DNS for *.%s at this host, or\n" +
" - unsetting LOCAL_DEV_ROOT_DOMAIN to use the mit.dev default.")
% (_probe_host, root_domain, root_domain)
)
# Parse prebuilt_tags list (["app=tag", ...]) into a lookup dict.
prebuilt_tags = {
kv.split("=")[0]: kv.split("=")[1]
for kv in cfg.get("prebuilt_tags", [])
if "=" in kv
}
# How long Loki keeps logs. Empty means the Pulumi program's own default (168h).
# Set it per-developer in the gitignored tilt_config.json — it is deliberately
# not pinned in Pulumi.local-dev.core.Dev.yaml, since Pulumi config would win
# over the environment and silently override this.
log_retention_period = cfg.get("log_retention_period") or os.environ.get("LOCAL_DEV_LOG_RETENTION", "")
# Workspace root: directory that contains ol-infrastructure and sibling app repos.
# Override with MITOL_WORKSPACE_ROOT environment variable.
workspace_root = os.environ.get("MITOL_WORKSPACE_ROOT", config.main_dir + "/..")
# ---------------------------------------------------------------------------
# Disk management
#
# Every image store bounds itself with retention config owned by the
# component that enforces it (details in the "Disk Management" section of
# local-dev/README.md):
# - disk-janitor (below): newest-N retention for tilt-built image tags in
# the local Docker daemon, plus a build-cache size cap.
# - zot registry: enforces its own retention + GC declaratively
# (local-dev/cluster/zot-config.json).
# - kubelet image GC: node containerd stores, thresholds in
# local-dev/cluster/k3d-config.yaml.
# - prune-docker (below): manual break-glass full cleanup.
# ---------------------------------------------------------------------------
# Tilt's own pruner, kept as a backstop only — disk-janitor (below) is
# authoritative for tilt-built images in the local daemon. keep_recent is
# derived from the same knob as the janitor so the two policies on that store
# can't disagree (a lower keep_recent would silently cap disk_keep_tags).
disk_keep_tags = int(cfg.get("disk_keep_tags") or os.environ.get("LOCAL_DEV_DISK_KEEP_TAGS", "") or "3")
docker_prune_settings(
disable=False,
max_age_mins=720, # 12h — the janitor handles day-to-day retention
num_builds=5, # also prune every 5 image builds
keep_recent=disk_keep_tags,
)
local_resource(
"disk-janitor",
serve_cmd="./local-dev/scripts/disk-janitor.sh",
deps=["./local-dev/scripts/disk-janitor.sh"],
serve_env={
"JANITOR_KEEP_TAGS": str(disk_keep_tags),
"JANITOR_BUILDCACHE_MAX_GB": cfg.get("disk_buildcache_max_gb") or os.environ.get("LOCAL_DEV_BUILDCACHE_MAX_GB", ""),
},
labels=["infra"],
)
# Break-glass full cleanup (Tilt UI button, or `tilt trigger prune-docker`).
# The janitor should make this unnecessary; expect image re-pulls and full
# rebuilds after running it.
local_resource(
"prune-docker",
cmd="./local-dev/scripts/prune-docker.sh",
labels=["infra"],
trigger_mode=TRIGGER_MODE_MANUAL,
auto_init=False,
)
# ---------------------------------------------------------------------------
# Application registry
#
# seed_commands: list of shell commands run via `kubectl exec` into the web
# pod after the deployment is healthy. Each entry is a dict with:
# - label: short name shown in Tilt UI
# - cmd: shell command to exec inside the pod
# All seed resources are TRIGGER_MODE_MANUAL — they never run automatically.
# ---------------------------------------------------------------------------
APPS = [
{
"name": "mit-learn",
"dir": "mit-learn",
"namespace": "mit-learn",
"deploy_name": "mitlearn-webapp",
"image_backend": "mitodl/mit-learn-app",
"image_frontend": "mitodl/mit-learn-nextjs-app",
"prebuilt_tag_backend": prebuilt_tags.get("mit-learn", "0.62.0"),
"prebuilt_tag_frontend": prebuilt_tags.get("mit-learn-nextjs", "0.62.0"),
"tiltfile": "./local-dev/apps/mit-learn/Tiltfile",
"tiltfile_frontend": "./local-dev/apps/mit-learn-nextjs/Tiltfile",
# Seeding commands; executed inside the web pod via `kubectl exec`.
# bootstrap runs automatically on first deploy (see apps/mit-learn/Tiltfile);
# the entries here are optional / on-demand enrichment tasks.
"seed_commands": [
{
"label": "seed-mit-learn-fixtures",
"description": "Load core fixtures: platforms, schools, departments, offered_by",
"cmd": "python manage.py loaddata platforms schools departments offered_by",
},
{
"label": "seed-mit-learn-qdrant",
"description": "Create Qdrant vector-search collections",
"cmd": "python manage.py create_qdrant_collections",
},
{
"label": "seed-mit-learn-opensearch",
"description": "Recreate OpenSearch index from scratch",
# --all is required: without an index selector the command just
# prints "Must select at least one index to update" and no-ops.
"cmd": "python manage.py recreate_index --all",
},
{
"label": "seed-mit-learn-ocw",
"description": "Backpopulate OCW learning resources (network access required)",
"cmd": "python manage.py backpopulate_ocw_data",
},
{
"label": "seed-mit-learn-mitxonline",
"description": "Backpopulate MITx Online resources",
"cmd": "python manage.py backpopulate_mitxonline_data",
},
{
"label": "seed-mit-learn-featured-lists",
# dev/rc-only command. Prereqs: offeror channels
# (backpopulate_resource_channels), published courses per
# offeror, and at least one user (author of the learning path).
"description": "Create dev-only featured lists per offeror channel",
"cmd": "python manage.py populate_featured_lists",
},
],
},
{
"name": "learn-ai",
"dir": "learn-ai",
"namespace": "learn-ai",
"deploy_name": "learnai-webapp",
"image_backend": "mitodl/learn-ai-app",
"prebuilt_tag_backend": prebuilt_tags.get("learn-ai", "0.28.3"),
"tiltfile": "./local-dev/apps/learn-ai/Tiltfile",
"seed_commands": [
{
"label": "seed-learn-ai-checkpoints",
"description": "Backpopulate tutor checkpoints from Open edX",
"cmd": "python manage.py backpopulate_tutor_checkpoints",
},
],
},
{
"name": "mitxonline",
"dir": "mitxonline",
"namespace": "mitxonline",
"deploy_name": "mitxonline-webapp",
"image_backend": "mitodl/mitxonline-app",
"prebuilt_tag_backend": prebuilt_tags.get("mitxonline", "1.144.5"),
"tiltfile": "./local-dev/apps/mitxonline/Tiltfile",
"seed_commands": [
{
"label": "seed-mitxonline-instance",
"description": "Full instance setup: superuser, OAuth2 app, program, courses, products",
"cmd": "python manage.py configure_instance",
},
{
"label": "seed-mitxonline-course-data",
"description": "Populate test course data from courses.json",
"cmd": "python manage.py populate_course_data",
},
{
"label": "seed-mitxonline-income-thresholds",
"description": "Load country income thresholds for financial assistance",
"cmd": "python manage.py load_country_income_thresholds flexiblepricing/fixtures/country_income_threshold_data.json",
},
],
},
{
"name": "odl-video-service",
"dir": "odl-video-service",
"namespace": "odl-video-service",
"deploy_name": "odlvideo-webapp",
"image_backend": "mitodl/odl-video-service-app",
"prebuilt_tag_backend": prebuilt_tags.get("odl-video-service", "0.85.0"),
"tiltfile": "./local-dev/apps/odl-video-service/Tiltfile",
"seed_commands": [
{
"label": "seed-ovs-presets",
"description": "Create video encoding presets",
"cmd": "python manage.py createpresets",
},
],
},
]
# ---------------------------------------------------------------------------
# Shared infrastructure (Pulumi stacks)
# ---------------------------------------------------------------------------
# Core stack (operators, foundational services, Keycloak instance)
local_resource(
"local-infra-core",
cmd="bash -c 'pulumi stack init local-dev.core.Dev 2>/dev/null; pulumi refresh --yes --skip-preview --stack local-dev.core.Dev && pulumi up --yes --skip-preview --logtostderr --stack local-dev.core.Dev'",
# Passed as environment rather than interpolated into the command string:
# log_retention_period is developer-supplied, and a value containing shell
# metacharacters would otherwise be interpreted here instead of reaching
# validate_retention_period() in observability.py.
env={
"LOCAL_DEV_ROOT_DOMAIN": root_domain,
"LOCAL_DEV_LOG_RETENTION": log_retention_period,
"PULUMI_CONFIG_PASSPHRASE": "",
},
dir="./local-dev/infra/core",
deps=["./local-dev/infra/modules", "./local-dev/infra/core/__main__.py"],
labels=["infra"],
)
# Apps infrastructure stack (databases, Keycloak realm) — depends on core.
#
# The Keycloak provider talks to the realm over the public ingress, so we wait
# for Keycloak's admin REST API to be ready before running `pulumi up`.
# Gating on the OIDC discovery endpoint alone is insufficient: discovery comes
# up well before the admin API can service writes, so the provider's
# POST /admin/realms races into the warm-up window and times out
# ("context deadline exceeded"). The script below proves the admin API is
# serving (admin token + authenticated GET /admin/realms) before we proceed.
#
# `pulumi refresh` is intentionally omitted from this loop: it issues a burst
# of concurrent admin-API calls on every reconcile, which was the main source
# of the warm-up 502s. Refresh on demand instead.
#
# `--parallel 1` serialises the apply for the same reason. The Keycloak provider
# talks to the admin API over the public ingress (host -> k3d LB -> APISIX ->
# Keycloak), and Pulumi otherwise creates independent realm resources
# concurrently. That burst of simultaneous admin calls overwhelms the ingress
# path (even while Keycloak itself is idle) and a batch of them trip
# "context deadline exceeded" together. Serialising keeps one admin request in
# flight at a time, which the path handles comfortably.
local_resource(
"local-infra-apps",
cmd="LOCAL_DEV_ROOT_DOMAIN={rd} PULUMI_CONFIG_PASSPHRASE='' bash -c '{wait} sso.ol.{rd} && {{ pulumi stack init local-dev.apps-infra.Dev 2>/dev/null; pulumi up --yes --skip-preview --parallel 1 --logtostderr --stack local-dev.apps-infra.Dev; }}'".format(rd=root_domain, wait="{}/local-dev/scripts/wait-for-keycloak-admin.sh".format(config.main_dir)),
dir="./local-dev/infra/apps_infra",
deps=["./local-dev/infra/modules", "./local-dev/infra/apps_infra/__main__.py", "./local-dev/scripts/wait-for-keycloak-admin.sh"],
labels=["infra"],
resource_deps=["local-infra-core"],
)
# Seed the local-dev test users (admin / student / prof, password localdev123)
# into the olapps realm. Pulumi provisions the realm and OIDC clients, but the
# Keycloak provider does not manage individual users, so they are created by
# this idempotent script. It runs automatically after the realm is up so a
# fresh setup has working login credentials — without it, every login fails
# because no users exist.
local_resource(
"kc-seed-users",
cmd="LOCAL_DEV_ROOT_DOMAIN={rd} {script}".format(rd=root_domain, script="{}/local-dev/scripts/kc-seed-users.sh".format(config.main_dir)),
deps=["./local-dev/scripts/kc-seed-users.sh"],
labels=["infra"],
resource_deps=["local-infra-apps"],
)
# Re-assert the realm's browser/first-broker-login flow bindings. The Keycloak
# provider resets them to built-in defaults whenever the keycloak.Realm resource
# is updated for an unrelated reason (see script comment) — Pulumi never
# reapplies the Bindings resource to fix it since its own inputs didn't change.
local_resource(
"kc-fix-flow-bindings",
cmd="LOCAL_DEV_ROOT_DOMAIN={rd} {script}".format(rd=root_domain, script="{}/local-dev/scripts/kc-fix-flow-bindings.sh".format(config.main_dir)),
deps=["./local-dev/scripts/kc-fix-flow-bindings.sh"],
labels=["infra"],
resource_deps=["local-infra-apps"],
)
# ---------------------------------------------------------------------------
# Per-app deployment + manual seed resources
# ---------------------------------------------------------------------------
for app in [a for a in APPS if a["name"] in enabled_apps]:
if os.path.exists(app["tiltfile"]):
include(app["tiltfile"])
# Include frontend Tiltfile if defined and present (e.g., mit-learn Next.js)
frontend_tiltfile = app.get("tiltfile_frontend", "")
if frontend_tiltfile and os.path.exists(frontend_tiltfile):
include(frontend_tiltfile)
# Register one manual-trigger Tilt resource per seed command.
# These are never auto-run; trigger them from the Tilt UI or with:
# tilt trigger seed-<app>-<label>
# or via:
# ./local-dev/scripts/seed.sh --app <app> --cmd <label>
for seed in app.get("seed_commands", []):
exec_cmd = (
"kubectl exec -n {ns} deploy/{deploy} -- {cmd}".format(
ns=app["namespace"],
deploy=app["deploy_name"],
cmd=seed["cmd"],
)
)
local_resource(
seed["label"],
cmd=exec_cmd,
resource_deps=[app["deploy_name"]],
labels=["seed"],
trigger_mode=TRIGGER_MODE_MANUAL,
auto_init=False,
)