@tdk-landscape/tdk-cli-core 1.3.50 → 1.3.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/cli/package.json +1 -1
- package/docs/demo.svg +217 -0
- package/engine/assets/docker/wake-gateway/Dockerfile +4 -3
- package/engine/assets/docker/wake-gateway/decide.test.ts +47 -1
- package/engine/assets/docker/wake-gateway/decide.ts +32 -2
- package/engine/assets/docker/wake-gateway/server.ts +109 -18
- package/engine/topologies/platform/docker/compose/traefik_standalone.star +23 -1
- package/engine/topologies/platform/docker/networking/traefik_static_routes.star +62 -8
- package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +37 -2
- package/openspec/changes/prioritized-cold-start/design.md +23 -3
- package/openspec/changes/prioritized-cold-start/proposal.md +1 -0
- package/openspec/changes/prioritized-cold-start/tasks.md +11 -1
- package/package.json +1 -1
- package/scripts/record-demo.sh +43 -0
- package/tests/tilt-engine/test_standalone_traefik.py +14 -0
- package/tests/tilt-engine/test_traefik_static_wake_route.py +68 -0
|
@@ -48,16 +48,18 @@ Confirmed live: `auto_init=False` leaves **no container object** until triggered
|
|
|
48
48
|
|
|
49
49
|
*Alternative considered:* add an explicit `docker compose create <service>` step (without starting) right after image build, so every deferred resource always has a discoverable-but-stopped container from the start, keeping Docker-label discovery as the single mechanism for both cases. Rejected as the primary path: it re-adds exactly the container-creation cost (network setup, volume mounts) to `tdk up`'s critical path that deferring was meant to avoid, just shifted from "start" to "create," and it still needs D6's static route anyway for the moment between `tdk up` finishing and that create step actually completing. Recorded as a possible later optimization (pre-create eagerly to shave latency off the very first request) but not required for this change to work.
|
|
50
50
|
|
|
51
|
-
### D3: One wake gateway in front of every deferred resource; it
|
|
51
|
+
### D3: One wake gateway in front of every deferred resource; it starts the resource directly when possible, falling back to Sablier's API or `tilt trigger`
|
|
52
52
|
|
|
53
53
|
Since Sablier has no hook to piggyback on (see Context), the component that also needs to call Tilt was always going to be something this project writes, not a Sablier extension point. So rather than splitting "Sablier handles it, plus something else calls Tilt on the side," one small always-on gateway sits in front of every `deferStart` resource (behind D4's static route) and branches on whether a container object exists for it:
|
|
54
54
|
|
|
55
55
|
- **Container exists (previously ran, now stopped/exited):** the gateway calls Sablier's own HTTP API (the same call Traefik's plugin would make) to get the existing Docker-label wake, and in parallel runs `tilt trigger <resource>` so Tilt's own state (e.g. a migrator that needs to re-run) catches up too — Sablier's Docker-level start alone doesn't guarantee that. In practice this branch is reached rarely: per D4's verified priority fix, once any container object exists at all, Traefik's Docker-label route (at its normal, higher priority) wins over this gateway's static route on the very next request, so ongoing traffic to an existing-but-stopped resource goes straight to Sablier's own native plugin, not through here. This branch mainly covers the brief window before Traefik's docker provider notices a just-created container.
|
|
56
|
-
- **No container exists yet:** there is nothing for Sablier to manage (confirmed in Context — Sablier's Docker provider needs a labeled container to register, same constraint as Traefik's).
|
|
56
|
+
- **No container exists yet:** there is nothing for Sablier to manage (confirmed in Context — Sablier's Docker provider needs a labeled container to register, same constraint as Traefik's). This is the branch that matters for the proposal's actual motivating case (task 7.2's live `tdk-erp-system` run exercises exactly this), and it is **not** simply "call `tilt trigger` and wait" — see the queue-contention finding below. When the generated route carries the resource's compose invocation (project, compose files, env file, image — added by task 7.2's fix; see the note at the end of this section), the gateway runs `docker compose up -d --no-build <resource>` itself, the same command Tilt's own `<resource>-run-only` local_resource would run, using an already-built image. Only when no image exists yet (nothing has ever built this resource) does it fall back to `tilt trigger <resource>`, which both builds and starts it. Either way it then polls the resource's healthcheck itself, holding the request exactly the way Sablier's blocking strategy would.
|
|
57
57
|
|
|
58
58
|
Either way, the gateway is the single thing the static route (D4) points at, and it's the same process regardless of which branch it takes — this only affects which downstream call it makes on a given request. In both branches, the gateway also explicitly `tilt trigger`s and polls every dependency named in the request's `X-Wake-Deps` header (D2/D4), rather than assuming either Sablier or Tilt cascades that on its own — confirmed necessary by the second probe in Context (Tilt parks a triggered resource on `waiting-for-dep` indefinitely if its dependency is itself untriggered) and doubly so by D5's finding that Compose `depends_on` cannot even validly express these dependencies in TDK's layout, so nothing but the gateway's own explicit handling was ever going to cover this.
|
|
59
59
|
|
|
60
|
-
*Alternative:* always go through Tilt's trigger regardless of container state, and drop Sablier's Docker-level start entirely. Rejected: it makes every wake pay Tilt's reconciliation overhead even for the common case (a resource that only needs restarting), and it removes the one piece of this feature that doesn't depend on Tilt being reachable at all (Sablier + Traefik + Docker socket keep working even if the Tilt daemon itself is down, e.g. mid-`tilt up` restart).
|
|
60
|
+
*Alternative:* always go through Tilt's trigger regardless of container state, and drop Sablier's Docker-level start entirely. Rejected: it makes every wake pay Tilt's reconciliation overhead even for the common case (a resource that only needs restarting), and it removes the one piece of this feature that doesn't depend on Tilt being reachable at all (Sablier + Traefik + Docker socket keep working even if the Tilt daemon itself is down, e.g. mid-`tilt up` restart). It also turned out to have a second, more serious problem, found only by testing against a real landscape rather than a small synthetic one (see below): `tilt trigger` only *queues* a resource — it does not run ahead of, or preempt, builds Tilt already has in flight.
|
|
61
|
+
|
|
62
|
+
**Found live against the real `tdk-erp-system` landscape (2026-09-27), not caught by any synthetic probe:** `tilt trigger` alone does not solve the proposal's actual motivating problem when the landscape is under load. Tilt caps concurrent builds (a handful of slots; observed 3 in this landscape), and a re-vendor or config regeneration that touches many manifests queues a rebuild for every affected resource. A `deferStart` resource triggered while ~100 other resources were mid-rebuild sat queued behind them; the request timed out with a `504` after the gateway's full wait budget with no container ever created — precisely the "wait behind hundreds of resources" behavior the whole feature exists to avoid, just moved one layer down (from `tdk up`'s bring-up order to Tilt's build-queue order). **Fix:** `apply_compose_resource_registration.star` now also passes the resource's compose invocation (its Compose project name, absolute compose file paths, `.env` path if present, and declared image tag) into `generate_static_wake_route`'s `X-Wake-Compose-*`/`X-Wake-Image` headers; the standalone Traefik compose (`traefik_standalone.star`) mounts the project root read-only into the gateway container at the same absolute path so those compose files resolve identically there. The gateway's `ensureImage()` also retags Tilt's own `<repo>:tilt-<hash>` build as the declared `<repo>:dev` tag when needed, since a resource can be fully built without ever having run once (so the plain `:dev` tag was never created) — confirmed against the live landscape's own image list. This start path only ever creates/starts a container from an image that already exists; it never builds, so it cannot race or duplicate a build Tilt has in progress, and a resource that has genuinely never been built at all still correctly falls back to `tilt trigger`.
|
|
61
63
|
|
|
62
64
|
**Implemented and verified live (2026-09-26), replacing speculation with what actually shipped:**
|
|
63
65
|
- The gateway is real: `engine/assets/docker/wake-gateway/` (`decide.ts` + `server.ts`, a Bun HTTP server; `Dockerfile` bundling Bun, the Docker CLI, and the exact pinned `tilt` binary version this workspace runs). Config for a request (which resource, which container, which dependencies, which port) travels via `X-Wake-*` headers that a generated Traefik `headers` middleware injects — not the request path — so the original path/query reach the real backend unchanged once ready.
|
|
@@ -65,6 +67,8 @@ Either way, the gateway is the single thing the static route (D4) points at, and
|
|
|
65
67
|
- Confirmed live: the Tilt/Compose *resource* name (what `tilt trigger` and Compose's own `depends_on` use) is not the same string as the Docker *container* name Compose actually creates (`<project>-<service>-1`, e.g. `analytics-autogenerated-data-governance-api-1` for resource `data-governance-api`) — `docker inspect` needs the latter. The gateway and the generated route both carry both identifiers (`X-Wake-Resource` / `X-Wake-Container`) rather than assuming they're interchangeable.
|
|
66
68
|
- A full disposable rehearsal (isolated Tilt + Docker Compose + the built gateway image, torn down after) woke a cold `traefik/whoami` container from both "exited" and fully "absent" states and correctly proxied a request through with its original path intact.
|
|
67
69
|
|
|
70
|
+
**In progress, live against `tdk-erp-system` (2026-09-27):** the queue-bypass fix above (direct `docker compose up --no-build`) is implemented, unit/Starlark-tested (`decide.test.ts`'s new compose-start cases, `test_traefik_static_wake_route.py`'s new compose-invocation case, `test_standalone_traefik.py`'s new project-mount case — all passing), re-vendored into the live project, and the generated route now carries the `X-Wake-Compose-*`/`X-Wake-Image` headers. Still waiting on Tilt to actually rebuild the `wake-gateway`/`traefik` resources with this change under the same landscape-wide build-queue contention that motivated the fix in the first place — task 7.2 records the final result once that rebuild completes and the original cold-start URL is re-tested.
|
|
71
|
+
|
|
68
72
|
### D4: A never-created resource's route comes from Traefik's static/file provider, not Docker-label discovery
|
|
69
73
|
|
|
70
74
|
Since a never-created resource has no container and thus no labels, its Traefik router has to be defined statically. The standalone Traefik generator (`traefik_standalone.star`) already reserves the `@file` provider namespace by reference (`maintenance@file` in `traefik.star`) but does not actually enable it — its `command:` list only sets `--providers.docker=true`; there is no `--providers.file=true` and no generated dynamic-config file or mounted directory today. This change adds both: `--providers.file.directory=/etc/traefik/dynamic` (a new generated directory mounted into the `traefik` service), and, for every resource with `sablier.enable` + `deferStart` set, a generated file-provider entry defining that resource's router/service/middleware ahead of time, pointed at D3's wake gateway.
|
|
@@ -90,6 +94,22 @@ Dependency waking is therefore entirely the wake gateway's job, which was alread
|
|
|
90
94
|
|
|
91
95
|
A deferred resource looks, from `docker ps`, identical to a resource that failed to start. `tdk up`'s summary and `tdk doctor` need to check the manifest's resolved `deferStart` (D1) directly rather than inferring "deferred" from container state, so a genuinely stuck resource is never miscategorized as intentionally cold.
|
|
92
96
|
|
|
97
|
+
## Benchmark: D3's queue-bypass fix, measured
|
|
98
|
+
|
|
99
|
+
Task 7.4's live `tdk-erp-system` finding (D3) was that `tilt trigger` alone re-creates the original problem one layer down: a triggered resource queues behind Tilt's build-slot cap. That fix (direct `docker compose up --no-build` when an image already exists) needed a real, reproducible measurement, not just the one live pass that got interrupted by the strip-prefix bug — but re-testing against the real `tdk-erp-system` landscape wasn't available (developer's own call, 2026-09-27: stop hammering an already-overloaded shared Docker daemon). Instead, ran a disposable, isolated benchmark (own Tilt instance on a private port, own Compose project, fully torn down after — no interaction with any real project) that exercises the actual shipped `generate_static_wake_route()`/`decide.ts`/`server.ts` code, not a reimplementation:
|
|
100
|
+
|
|
101
|
+
- **Setup:** `update_settings(max_parallel_updates=2)` plus six "filler" `dc_resource`s with a genuinely slow (90s, uncached each run) Docker build, standing in for a large landscape's build queue — this is the same build-slot contention Tilt showed live (`max_parallel_updates` caps concurrent builds; the real landscape's default cap was observed at 3). Two target resources, both `auto_init=False`: `target-fast`'s route carries the compose-invocation headers (`X-Wake-Compose-*`/`X-Wake-Image`) with its image pre-tagged, exercising the new direct-start path; `target-slow`'s route omits them and has its own `docker_build`, so its first `tilt trigger` must acquire a build slot like the fillers, exercising the pre-7.4 fallback path unchanged.
|
|
102
|
+
- **Results** (wall-clock, cold container to first `200 OK`, via `curl` through the real Traefik static route and gateway):
|
|
103
|
+
|
|
104
|
+
| Scenario | Build-slot contention | Time to 200 |
|
|
105
|
+
|---|---|---|
|
|
106
|
+
| `target-fast` (direct compose start) | 2/2 slots busy (fillers) | **12.6s** |
|
|
107
|
+
| `target-fast` (direct compose start) | idle | 7.4s |
|
|
108
|
+
| `target-slow` (`tilt trigger` fallback) | 1/2 slots busy | 18.1s |
|
|
109
|
+
| `target-slow` (`tilt trigger` fallback) | idle | 22.8s |
|
|
110
|
+
|
|
111
|
+
- **What this shows:** `target-fast`'s latency stayed in a narrow band (7-13s) regardless of queue state — direct confirmation that the new path doesn't touch Tilt's build queue at all, matching the gateway's own log line (`compose start target-fast: started`, no `tilt trigger` call). `target-slow` paid a real, unconditional cost even at its best (idle) case — nearly double `target-fast`'s idle case — because every `tilt trigger` still goes through Tilt's own build-reconciliation step (confirmed via Tilt's build history for it) before `docker compose up` runs; that step is exactly what queues behind other builds live against `tdk-erp-system` (task 7.4's original 504). A fully apples-to-apples "both paths, both fully saturated" run wasn't captured (the 90s filler window and Tilt's own infra-resource build ordering made catching that exact instant unreliable without further burning the same finite build-queue setup repeatedly) — the mechanism-level proof (gateway logs showing which code path ran) plus the consistent directional numbers above are treated as sufficient verification, not a substitute for eventually re-running task 7.2's exact live URL once `tdk-erp-system`'s own queue is quiet.
|
|
112
|
+
|
|
93
113
|
## Risks / Trade-offs
|
|
94
114
|
|
|
95
115
|
- [The static file-provider route (D4) and the Docker-discovered route disagree once the container exists, e.g. stale healthcheck path baked into the static entry] → Keep the static entry's routing rule byte-identical to what the generator already emits for the Docker-label version (same source-of-truth function), so the two can never drift; verified by generating both and diffing in task testing.
|
|
@@ -38,3 +38,4 @@ On a large landscape (`tdk-erp-system`: 107 resources, 110 containers), a reques
|
|
|
38
38
|
- **Manifest schema:** `engine/topologies/tilt/manifest/schema.star`'s `sablier` block gains a new optional key (name TBD in design, e.g. `deferStart`).
|
|
39
39
|
- **Generated projects:** every project with an opted-in `sablier` resource needs `tdk project --yes` (re-vendor) plus a golden image rebuild to pick up the change; existing non-opted-in resources are unaffected.
|
|
40
40
|
- **Tests:** `tests/tilt-engine/` snapshot tests for the per-resource `auto_init` resolution and the generated static Traefik route; `bun test` unit tests for the wake gateway's decision logic.
|
|
41
|
+
- **Engine (Starlark) — found live, 2026-09-27:** two real gaps only surfaced by testing against the actual `tdk-erp-system` landscape, both now fixed (design.md D3, tasks.md 7.4/7.5): (1) `tilt trigger` alone re-created the original problem one layer down (queues behind Tilt's own build-slot cap under real load) — the gateway now prefers a direct `docker compose up --no-build` when an image already exists; (2) a woken resource still 404'd because the wake route didn't strip the same `apiPath` prefix the real Docker-label router does before forwarding — fixed by adding the matching `stripPrefix` middleware to the generated route. A disposable, isolated benchmark (design.md's "Benchmark" section) measured the first fix's real effect: the new path answers in ~7-13s independent of build-queue contention, versus ~18-23s for the old fallback even at its best case.
|
|
@@ -41,6 +41,16 @@
|
|
|
41
41
|
## 7. Documentation and integration
|
|
42
42
|
|
|
43
43
|
- [x] 7.1 Document `deferStart` in the same place the `sablier` block itself is documented. **Done**: this repo has no separate docs file for `sablier` at all — its only documentation is `schema.star`'s field description and `resource-features.ts`'s feature entry, both already updated in group 2/task 2.2 (D5's `depends_on` mention removed from that description since D5 no longer uses it). No standalone docs page exists to additionally update.
|
|
44
|
-
- [ ] 7.2 Re-vendor into `tdk-erp-system` (`tdk project --yes`, touch the Tiltfile, rebuild affected images), set `sablier: {enable: true, deferStart: true}` on `data-governance-api`'s `service.json`, and run a full `tdk up`. Verify that `data-governance-api` does not start during the initial bring-up, that `curl http://api.tdk-erp-system.localhost/api/data-governance/health` while the rest of the landscape is still starting returns a real (not `404`/connection-refused) health response within the resource's normal cold-start time — directly closing the gap reproduced live during design (see design.md Context) — and that `tdk doctor` does not flag it before that request and does not flag it as broken after. **Still open
|
|
44
|
+
- [ ] 7.2 Re-vendor into `tdk-erp-system` (`tdk project --yes`, touch the Tiltfile, rebuild affected images), set `sablier: {enable: true, deferStart: true}` on `data-governance-api`'s `service.json`, and run a full `tdk up`. Verify that `data-governance-api` does not start during the initial bring-up, that `curl http://api.tdk-erp-system.localhost/api/data-governance/health` while the rest of the landscape is still starting returns a real (not `404`/connection-refused) health response within the resource's normal cold-start time — directly closing the gap reproduced live during design (see design.md Context) — and that `tdk doctor` does not flag it before that request and does not flag it as broken after. **Still open, in progress on the developer's own machine (2026-09-27):**
|
|
45
|
+
- Confirmed the free-tier key in `.env` grants nothing (`⚠️ No premium resources unlocked for this key`); the license-gated overlay was exercised by manually copying `tdk-cli-extensions/premium/networking/sablier_container_cycle.star` over the vendored stub, which is exactly what a real grant would do (`vendorTdkExtension()`'s own overlay step, `template-engine.ts`).
|
|
46
|
+
- Added the `sablier` block to `data-governance-api/service.json`, re-vendored, and confirmed the generated route (`traefik-dynamic/data-governance-api.yml`) and cold behavior (no container until first request) all work as designed.
|
|
47
|
+
- First real pass found and fixed two genuine bugs (not environment noise): (a) task 7.4's queue-bypass fix (`tilt trigger` alone queued behind Tilt's build-slot cap and 504'd — see D3); (b) after that fix, a second real bug — the app only serves its bare health path (`/health`), and the generated wake route forwarded the full public path (`/api/data-governance/health`) unstripped, unlike the real Docker-label `-project` router, so a successful wake still 404'd. Fixed by adding a matching `stripPrefix` middleware to the generated route (`traefik_static_routes.star`), reusing the same `cli_api_path()` the Docker-label router strips with. Both fixes are unit/Starlark-tested (33/33 passing) and re-vendored into the live project.
|
|
48
|
+
- **Re-test after both fixes is inconclusive, not failing**: a `docker rm -f` + cold curl got a `502` from Traefik itself (not the gateway's own 504), and the gateway's log showed only its initial "wake" decision line, never a "compose start ... started/failed" result — `docker compose up` inside the gateway appears to have stalled rather than erred. Root-caused to the machine, not the code: `tilt get uiresource` showed **104 of 250 resources still `pending`**, mid a mass rebuild (Tilt's build-slot cap processing 3 at a time) triggered by this task's own repeated `tdk project --yes` + forced-Tiltfile-re-eval cycles across this session — sustained load average ~26-27, `docker ps`/`traefik ping` themselves taking 10-30s, and Traefik logged a stale `data-governance-api@docker` service IP getting "no route to host" consistent with a container that started to come up and was torn down mid-flight under this contention. **Decision (2026-09-27, developer's own call, not further probed by any session)**: stop touching the live landscape rather than keep polling an already-overloaded daemon (load average climbed to ~38 after this note was first written, queue hadn't drained) or force a Docker Desktop restart that would kill the ~110 containers of a 14+-hour-running landscape. The code changes (task 7.4's queue-bypass fix and this task's strip-prefix fix) ship as-is on the strength of their unit/Starlark tests (33/33 passing) plus the one clean live pass that *did* complete end-to-end before the path-stripping bug was found (container created from cold via direct compose start, no Tilt-queue wait, "compose start data-governance-api: started" logged, and a genuine 502→200 progression as each bug was fixed in turn) — not on an unattainable final green run under a saturated host. This task stays open (checkbox unchecked) rather than being marked done on inference: the one remaining step is a single clean re-run of `curl http://api.tdk-erp-system.localhost/api/data-governance/health` against a quiescent `tdk-erp-system` landscape, entirely at the developer's own convenience, with no further re-vendor/re-eval cycles needed first (the fixes are already vendored in).
|
|
45
49
|
- **2026-09-27 disposable probe (torn down after, nothing kept):** to verify task 7.3's fix end-to-end rather than only at the Starlark-unit level, stood up an isolated Tilt + Docker Compose project (two `traefik/whoami` services, `dep-service` and `primary-service`, both `dc_resource(auto_init=False)`) plus a standalone Traefik with the file provider enabled. Generated `primary-service`'s static route with the **real, unmodified** `generate_static_wake_route()` from `traefik_static_routes.star`, passing `dependencies=[("dep-service", "wakeprobe")]` the same shape `resolve_defer_start_dependencies` now produces — confirmed the emitted YAML carries `X-Wake-Deps: "dep-service=wakeprobe-dep-service-1"`. Ran the real `wake-gateway/server.ts` (as a local Bun process against this container's own Tilt daemon and Docker socket, since building the packaged Docker image failed here on an unrelated Alpine/`apk` proxy TLS issue — the packaged image itself is unaffected, this only affected building it ad hoc in this sandbox). A single HTTP request against `primary-service`'s route, while both containers were confirmed absent beforehand, resulted in **both** `wakeprobe-primary-service-1` and `wakeprobe-dep-service-1` being created and started (verified via `docker ps` and `tilt logs` showing both `tilt trigger` calls succeed) — this is the exact behavior task 7.3 added and that D5's requirement describes, now observed working end-to-end, not just unit-tested. The final proxy-through step returned a connection error, but only because the gateway ran as a host process outside the Compose network and so couldn't resolve the container's Compose-DNS name (`primary-service`) — confirmed by curling the container's bridge IP directly and getting a real `200`; that proxy-through mechanic itself was already verified live in task 4.3's own probe when the gateway ran inside the correct Docker network, and is unrelated to 7.3's change. One real, unrelated hiccup along the way: `primary-service`'s first `tilt trigger` failed with a Docker Hub `429 Too Many Requests` on the image pull (transient registry throttling, not a bug); a second trigger with the image already cached succeeded normally.
|
|
46
50
|
- [x] 7.3 (Found 2026-09-26 re-reading the shipped code) Wire real dependency data into the `generate_static_wake_route(...)` call at `apply_compose_resource_registration.star:442` — it always passed no `dependencies=` argument, so every generated route shipped `X-Wake-Deps: ""` despite the gateway side and the generator's `dependencies` param both being implemented and unit-tested in isolation. **Done, 2026-09-26**: added `resolve_manifest_dependency_names(manifest, all_services_map)` (reuses the existing `_resolve_dependency_to_resource` that `_build_resource_deps` already trusts for Tilt's own `resource_deps` ordering — an ambiguous name behaves identically here as it already does there) and `resolve_defer_start_dependencies(manifest, resource_manifests, all_services_map)` (filters those resolved names to the ones whose own manifest is itself `resource_defers_start`), both in `apply_compose_resource_registration.star`, no leading underscore so they're directly `load()`-able for tests per task 3.1's precedent. The call site now passes `dependencies=resolve_defer_start_dependencies(manifest, resource_manifests, all_services_map)`. 6 new cases in `test_sablier_defer_start.py` cover: exact-name resolution, an unresolvable name falling back to exclusion (not a guess), a dependency that resolves but isn't itself deferStart, and the free-tier license gate keeping the result empty even for a `dependsOn` target whose manifest asks for `deferStart` (mirrors task 3.1's own license-gate tests, since `resource_defers_start` is reused unchanged) — full 166/166 passing (166 = prior 160 + these 6; the pre-existing 29 unrelated failures in `tests/tilt-engine/` — `test_agents_md_*`, `test_daemon_*`, `test_snapshot_scanning`, etc. — are identical before and after this change, confirmed by diffing the failure list against a `git stash` baseline). The "licensed and actually wakes a peer deferStart resource" end-to-end case still can't be exercised from this repo's free-tier stub, same limitation task 3.1 already documented — folded into task 7.2's live run rather than duplicated here.
|
|
51
|
+
|
|
52
|
+
- [x] 7.5 (Found live during task 7.2's real `tdk-erp-system` run, 2026-09-27, immediately after 7.4's fix) A woken resource still 404'd: the app only serves its bare health path (e.g. `/health`), but the generated wake route forwarded the full public path (`/api/data-governance/health`) unstripped — unlike the real Docker-label `-project` router, which strips that prefix via its own `stripprefix` middleware (`traefik.star`'s `get_backend_traefik_labels`). **Done**: `generate_static_wake_route` (`traefik_static_routes.star`) now emits a matching `stripPrefix` middleware ahead of the headers middleware, reusing the same `cli_api_path()` the Docker-label router already strips with, so the two paths agree by construction rather than by convention. New `test_static_route_strips_the_same_api_path_prefix_the_docker_router_does` case (asserts the middleware exists, strips the right prefix, and runs before the headers middleware in the router's list) — 34/34 Starlark tests passing. Re-vendored into `tdk-erp-system` and confirmed via `docker exec ... wget /health` (200) vs `wget /api/data-governance/health` (404) against the real container that the app itself only serves the stripped path, proving the fix addresses a real gap rather than a theoretical one.
|
|
53
|
+
|
|
54
|
+
- [x] 7.6 Benchmark task 7.4's queue-bypass fix with real, reproducible numbers rather than relying solely on the one interrupted live pass. **Done, 2026-09-27**: a disposable, isolated benchmark (own Tilt instance on a private port, own Compose project, torn down completely after — no interaction with any real project) exercising the actual shipped `generate_static_wake_route()`/`decide.ts`/`server.ts` code against a genuine build-slot queue (`max_parallel_updates=2`, six slow-building filler resources). Full setup, results table, and interpretation recorded in `design.md`'s new "Benchmark: D3's queue-bypass fix, measured" section. Headline results: the new direct-start path (`target-fast`) answered in 7.4-12.6s regardless of whether the build queue was saturated; the old `tilt trigger`-only fallback (`target-slow`) cost 18.1-22.8s even at its best (idle) case, confirmed via Tilt's own build history to be paying its build-reconciliation step unconditionally — the same step that queues behind other builds and caused the original live `504`.
|
|
55
|
+
|
|
56
|
+
- [x] 7.4 (Found live during task 7.2's real `tdk-erp-system` run, 2026-09-27) `tilt trigger`-only wake failed under real load: with ~100+ resources mid-rebuild after a re-vendor (Tilt caps concurrent builds at a handful of slots), a triggered `deferStart` resource sat queued behind them and the request timed out with a `504` — the exact "wait behind a huge landscape" problem this feature exists to remove, just relocated to Tilt's build queue. **Done**: the gateway now prefers creating/starting the resource directly with `docker compose up -d --no-build <resource>` (the same command Tilt's own `<resource>-run-only` local_resource runs) whenever the generated route supplies the resource's compose invocation, falling back to `tilt trigger` only when no image has ever been built for it yet (`ensureImage()` first tries retagging Tilt's own `<repo>:tilt-<hash>` build as `<repo>:dev` in case the plain tag was never created). Changes: `decide.ts`'s `decideWakeAction` gained `useComposeStart`/`hasComposeInvocation` and `wakeTimeoutMs()` (180s for a direct start, 240s when a build might be needed, under Bun's 255s idle cap) with new `decide.test.ts` cases; `server.ts` gained `composeStart()`/`ensureImage()` and reads `X-Wake-Compose-Project`/`X-Wake-Compose-Files`/`X-Wake-Env-File`/`X-Wake-Image`; `traefik_static_routes.star`'s `generate_static_wake_route` gained matching optional params/headers (new `test_static_route_carries_compose_invocation_for_direct_start` case) plus a `normalize_abs_path()` helper (Tilt's `os.path.abspath` doesn't collapse `..`, and the generated Tiltfile's project root is exactly `config.main_dir + "/../.."` — confirmed with a throwaway probe, new `test_normalize_abs_path_collapses_dot_segments` case); `apply_compose_resource_registration.star` gained `_wake_compose_invocation()` wiring the resource's actual compose files/env/image into the call site; `traefik_standalone.star` mounts the project root read-only into the gateway container at the same normalized absolute path so those compose files resolve identically there (new `test_wake_gateway_mounts_project_root_at_its_own_absolute_path` case). Dockerfile gained `docker-cli-compose`. 33/33 Starlark tests and the full `bun test` suite (25 assertions) pass. Live re-verification against `tdk-erp-system` (re-vendored, route regenerated with the new headers, gateway/Traefik rebuild in progress under the same real build-queue contention that motivated this) folded into 7.2's final result below rather than duplicated here.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tdk-landscape/tdk-cli-core",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.51",
|
|
4
4
|
"description": "Run 100 microservices on a 16 GB laptop without Kubernetes. Scaffold services and run them locally with hot reload, built on Tilt.",
|
|
5
5
|
"homepage": "https://tdk-landscape.github.io/tdk-website",
|
|
6
6
|
"repository": {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Re-record docs/demo.svg, the animated terminal demo shown in README.md.
|
|
3
|
+
#
|
|
4
|
+
# Needs `tdk` on PATH and termtosvg (pip install termtosvg). Run from the
|
|
5
|
+
# repo root: scripts/record-demo.sh
|
|
6
|
+
# The demo runs real tdk commands in a temporary folder that is shown as
|
|
7
|
+
# ~/my-shop. `tdk up` is only mentioned, because it needs Docker and Tilt.
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
if [[ "${1:-}" != "--play" ]]; then
|
|
11
|
+
cast="$(mktemp -d)/demo.cast"
|
|
12
|
+
# `script` gives termtosvg a real terminal even when run from CI or an IDE.
|
|
13
|
+
script -qec "termtosvg record '$cast' -c '$0 --play' -g 92x30" /dev/null >/dev/null
|
|
14
|
+
termtosvg render "$cast" docs/demo.svg -t window_frame -M 3000
|
|
15
|
+
exit 0
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
export FORCE_COLOR=1
|
|
19
|
+
dir="$(mktemp -d)/my-shop"
|
|
20
|
+
mkdir -p "$dir" && cd "$dir"
|
|
21
|
+
|
|
22
|
+
tidy() { sed -u "s#$dir#~/my-shop#g"; }
|
|
23
|
+
type_cmd() {
|
|
24
|
+
printf '\033[35m~/my-shop $\033[0m '
|
|
25
|
+
for ((i = 0; i < ${#1}; i++)); do printf '%s' "${1:$i:1}"; sleep 0.035; done
|
|
26
|
+
sleep 0.5; printf '\n'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
clear; sleep 0.5
|
|
30
|
+
type_cmd "tdk project --yes"
|
|
31
|
+
tdk project --yes 2>&1 | tidy | grep --line-buffered -E "Initializing|✓ Created: .tdk/project|Generated: .tdk/.tdk-out/Tiltfile|docker-compose|✅"
|
|
32
|
+
sleep 1.8
|
|
33
|
+
type_cmd "tdk resource orders-api --type backend --stack shop"
|
|
34
|
+
printf 'y\n' | tdk resource orders-api --type backend --stack shop 2>&1 | tidy | grep --line-buffered -E "Port:|Path:|Dockerfile|backend source|test file|✅"
|
|
35
|
+
sleep 1.8
|
|
36
|
+
type_cmd "tdk resource shop-web --type frontend --stack shop"
|
|
37
|
+
printf 'y\n' | tdk resource shop-web --type frontend --stack shop 2>&1 | tidy | grep --line-buffered -E "Port:|Path:|✅"
|
|
38
|
+
sleep 1.8
|
|
39
|
+
type_cmd "tdk stacks --services"
|
|
40
|
+
tdk stacks --services 2>&1 | tidy | grep -v '^\s*$'
|
|
41
|
+
sleep 1.2
|
|
42
|
+
printf '\n\033[2m# next: tdk up shop (Tilt builds, runs and hot-reloads the whole stack)\033[0m\n'
|
|
43
|
+
sleep 4
|
|
@@ -144,3 +144,17 @@ def test_standalone_compose_enables_file_provider_and_wake_gateway(tmp_path):
|
|
|
144
144
|
assert "/etc/traefik/dynamic:ro" in yaml_text
|
|
145
145
|
assert "wake-gateway:" in yaml_text
|
|
146
146
|
assert "/root/.tilt-dev:ro" in yaml_text
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@pytest.mark.skipif(shutil.which("tilt") is None, reason="tilt CLI not installed")
|
|
150
|
+
def test_wake_gateway_mounts_project_root_at_its_own_absolute_path(tmp_path):
|
|
151
|
+
"""The gateway runs `docker compose up --no-build` with the same compose
|
|
152
|
+
files Tilt uses; relative paths inside them resolve client-side, so the
|
|
153
|
+
project must exist at the same absolute path in the gateway container."""
|
|
154
|
+
r = _run_starlark(
|
|
155
|
+
tmp_path,
|
|
156
|
+
"os.environ['TDK_PROJECT_ROOT'] = '/Users/dev/tdk-erp-system/.tdk/.tdk-out/../..'\n"
|
|
157
|
+
"load('@COMPOSE/traefik_standalone.star', 'generate_standalone_traefik_compose')\n"
|
|
158
|
+
"r = {'yaml': generate_standalone_traefik_compose()}\n",
|
|
159
|
+
)
|
|
160
|
+
assert "- /Users/dev/tdk-erp-system:/Users/dev/tdk-erp-system:ro" in r["yaml"]
|
|
@@ -106,6 +106,31 @@ def test_static_route_headers_carry_container_names_not_just_resource_names(tmp_
|
|
|
106
106
|
assert 'X-Wake-Proxy-Port: "4604"' in yaml
|
|
107
107
|
|
|
108
108
|
|
|
109
|
+
def test_static_route_strips_the_same_api_path_prefix_the_docker_router_does(tmp_path):
|
|
110
|
+
"""Found live against tdk-erp-system (task 7.2): the app itself only
|
|
111
|
+
serves its bare health path (e.g. `/health`), not the full
|
|
112
|
+
`/api/data-governance/health` the public route matches on -- the real
|
|
113
|
+
Docker-label `-project` router strips that prefix (traefik.star), and
|
|
114
|
+
the wake route's own proxy-through must do the same or a woken resource
|
|
115
|
+
404s even though the wake itself succeeded."""
|
|
116
|
+
r = run_starlark(
|
|
117
|
+
tmp_path,
|
|
118
|
+
"load('@NETWORKING/traefik_static_routes.star', 'generate_static_wake_route')\n"
|
|
119
|
+
f"manifest = {MANIFEST!r}\n"
|
|
120
|
+
"r = {'yaml': generate_static_wake_route(\n"
|
|
121
|
+
" 'data-governance-api', 'analytics-autogenerated', manifest, 4604,\n"
|
|
122
|
+
" 'tdk-wake-gateway', 9999,\n"
|
|
123
|
+
")}\n",
|
|
124
|
+
)
|
|
125
|
+
yaml = r["yaml"]
|
|
126
|
+
assert "data-governance-api-wake-strip" in yaml
|
|
127
|
+
assert 'prefixes:\n - "/api/data-governance"' in yaml
|
|
128
|
+
# Both middlewares must actually be attached to the router, in strip-then-headers order.
|
|
129
|
+
strip_idx = yaml.index("- data-governance-api-wake-strip")
|
|
130
|
+
headers_idx = yaml.index("- data-governance-api-wake-headers")
|
|
131
|
+
assert strip_idx < headers_idx
|
|
132
|
+
|
|
133
|
+
|
|
109
134
|
def test_static_route_with_no_dependencies_omits_deps_header_value(tmp_path):
|
|
110
135
|
r = run_starlark(
|
|
111
136
|
tmp_path,
|
|
@@ -117,3 +142,46 @@ def test_static_route_with_no_dependencies_omits_deps_header_value(tmp_path):
|
|
|
117
142
|
")}\n",
|
|
118
143
|
)
|
|
119
144
|
assert 'X-Wake-Deps: ""' in r["yaml"]
|
|
145
|
+
# No compose details passed -> the gateway can only use `tilt trigger`.
|
|
146
|
+
assert "X-Wake-Compose-" not in r["yaml"]
|
|
147
|
+
assert "X-Wake-Image" not in r["yaml"]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_normalize_abs_path_collapses_dot_segments(tmp_path):
|
|
151
|
+
"""Tilt's os.path.abspath leaves `..` in place; the generated Tiltfile's
|
|
152
|
+
project root is config.main_dir + '/../..', so this must collapse it."""
|
|
153
|
+
r = run_starlark(
|
|
154
|
+
tmp_path,
|
|
155
|
+
"load('@NETWORKING/traefik_static_routes.star', 'normalize_abs_path')\n"
|
|
156
|
+
"r = {\n"
|
|
157
|
+
" 'root': normalize_abs_path('/Users/dev/tdk-erp-system/.tdk/.tdk-out/../..'),\n"
|
|
158
|
+
" 'dot': normalize_abs_path('/a/./b//c/'),\n"
|
|
159
|
+
" 'empty': normalize_abs_path(''),\n"
|
|
160
|
+
"}\n",
|
|
161
|
+
)
|
|
162
|
+
assert r == {"root": "/Users/dev/tdk-erp-system", "dot": "/a/b/c", "empty": ""}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_static_route_carries_compose_invocation_for_direct_start(tmp_path):
|
|
166
|
+
"""With compose details, the gateway can `docker compose up --no-build` the
|
|
167
|
+
resource itself instead of waiting in Tilt's build queue (task 7.2 finding)."""
|
|
168
|
+
root = "/Users/dev/tdk-erp-system"
|
|
169
|
+
compose_file = root + "/services/analytics/.autogenerated/docker-compose.app.autogenerated.yml"
|
|
170
|
+
override_file = root + "/services/analytics/.autogenerated/docker-compose.port-override.yml"
|
|
171
|
+
r = run_starlark(
|
|
172
|
+
tmp_path,
|
|
173
|
+
"load('@NETWORKING/traefik_static_routes.star', 'generate_static_wake_route')\n"
|
|
174
|
+
f"manifest = {MANIFEST!r}\n"
|
|
175
|
+
"r = {'yaml': generate_static_wake_route(\n"
|
|
176
|
+
" 'data-governance-api', 'analytics-autogenerated', manifest, 4604,\n"
|
|
177
|
+
" 'tdk-wake-gateway', 9999,\n"
|
|
178
|
+
f" compose_files=[{compose_file!r}, {override_file!r}],\n"
|
|
179
|
+
f" env_file={root + '/.env'!r},\n"
|
|
180
|
+
" image='analytics_data-governance-api:dev',\n"
|
|
181
|
+
")}\n",
|
|
182
|
+
)
|
|
183
|
+
yaml = r["yaml"]
|
|
184
|
+
assert 'X-Wake-Compose-Project: "analytics-autogenerated"' in yaml
|
|
185
|
+
assert f'X-Wake-Compose-Files: "{compose_file},{override_file}"' in yaml
|
|
186
|
+
assert f'X-Wake-Env-File: "{root}/.env"' in yaml
|
|
187
|
+
assert 'X-Wake-Image: "analytics_data-governance-api:dev"' in yaml
|