@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.
@@ -1,8 +1,9 @@
1
1
  # Wake gateway (see openspec/changes/prioritized-cold-start): fronts every
2
2
  # `sablier.deferStart` resource's Traefik static route, triggers cold
3
3
  # containers via `tilt trigger` and/or Sablier, then holds the request until
4
- # healthy. Needs the Docker CLI (client only, talks to the mounted
5
- # /var/run/docker.sock) and the real `tilt` CLI (D3: shells out to the
4
+ # healthy. Needs the Docker CLI plus its Compose plugin (client only, talks
5
+ # to the mounted /var/run/docker.sock; compose starts an already-built image
6
+ # directly, ahead of Tilt's build queue) and the real `tilt` CLI (D3: shells out to the
6
7
  # documented `tilt trigger` command rather than depending on Tilt's
7
8
  # undocumented internal HTTP API).
8
9
 
@@ -13,7 +14,7 @@ FROM oven/bun:1.3.11-alpine AS base
13
14
  # directly (not via tilt-dev's install.sh, which shells out to `sudo` -- not
14
15
  # present in this minimal image, and unnecessary since we're already root).
15
16
  ARG TILT_VERSION=0.37.7
16
- RUN apk add --no-cache docker-cli curl gcompat \
17
+ RUN apk add --no-cache docker-cli docker-cli-compose curl gcompat \
17
18
  && arch="$(uname -m)" \
18
19
  && case "$arch" in aarch64) tilt_arch=arm64 ;; x86_64) tilt_arch=x86_64 ;; *) echo "unsupported arch: $arch" >&2; exit 1 ;; esac \
19
20
  && curl -fsSL "https://github.com/tilt-dev/tilt/releases/download/v${TILT_VERSION}/tilt.${TILT_VERSION}.linux.${tilt_arch}.tar.gz" \
@@ -1,5 +1,11 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { decideWakeAction, evaluateReadiness } from "./decide";
2
+ import {
3
+ COMPOSE_START_TIMEOUT_MS,
4
+ TILT_TRIGGER_TIMEOUT_MS,
5
+ decideWakeAction,
6
+ evaluateReadiness,
7
+ wakeTimeoutMs,
8
+ } from "./decide";
3
9
 
4
10
  describe("decideWakeAction", () => {
5
11
  test("never-created resource: tilt trigger only, no Sablier start", () => {
@@ -56,6 +62,31 @@ describe("decideWakeAction", () => {
56
62
  ]);
57
63
  });
58
64
 
65
+ test("never-created resource with compose details: started directly, not queued in Tilt", () => {
66
+ const action = decideWakeAction({
67
+ resourceContainerName: "data-governance-api",
68
+ resourceState: "absent",
69
+ dependencies: [{ name: "peer-api", state: "absent" }],
70
+ hasComposeInvocation: true,
71
+ });
72
+ expect(action.useComposeStart).toBe(true);
73
+ // Dependencies carry no compose details, so they still go through Tilt.
74
+ expect(action.tiltTriggerNames).toEqual(["peer-api"]);
75
+ expect(action.useSablierStart).toBe(false);
76
+ });
77
+
78
+ test("existing (exited) container keeps the Sablier path even with compose details", () => {
79
+ const action = decideWakeAction({
80
+ resourceContainerName: "data-governance-api",
81
+ resourceState: "exited",
82
+ dependencies: [],
83
+ hasComposeInvocation: true,
84
+ });
85
+ expect(action.useComposeStart).toBe(false);
86
+ expect(action.tiltTriggerNames).toEqual(["data-governance-api"]);
87
+ expect(action.useSablierStart).toBe(true);
88
+ });
89
+
59
90
  test("running dependency is not re-triggered", () => {
60
91
  const action = decideWakeAction({
61
92
  resourceContainerName: "data-governance-api",
@@ -66,6 +97,21 @@ describe("decideWakeAction", () => {
66
97
  });
67
98
  });
68
99
 
100
+ describe("wakeTimeoutMs", () => {
101
+ test("a direct compose start only waits for boot + healthcheck", () => {
102
+ expect(wakeTimeoutMs(true)).toBe(COMPOSE_START_TIMEOUT_MS);
103
+ });
104
+
105
+ test("the tilt trigger path allows for an image build, under Bun's 255s idle cap", () => {
106
+ expect(wakeTimeoutMs(false)).toBe(TILT_TRIGGER_TIMEOUT_MS);
107
+ expect(TILT_TRIGGER_TIMEOUT_MS).toBeLessThan(255_000);
108
+ });
109
+
110
+ test("an explicit X-Wake-Timeout-Ms wins", () => {
111
+ expect(wakeTimeoutMs(false, 5_000)).toBe(5_000);
112
+ });
113
+ });
114
+
69
115
  describe("evaluateReadiness", () => {
70
116
  test("ready once every required name reports healthy", () => {
71
117
  const outcome = evaluateReadiness(["a", "b"], { a: true, b: true });
@@ -20,11 +20,26 @@ export interface WakeRequestSnapshot {
20
20
  resourceContainerName: string;
21
21
  resourceState: ContainerState;
22
22
  dependencies: DependencySnapshot[];
23
+ /**
24
+ * Whether the route carried enough compose detail (project, files, image)
25
+ * for the gateway to create the resource's container itself.
26
+ */
27
+ hasComposeInvocation?: boolean;
23
28
  }
24
29
 
25
30
  export interface WakeAction {
26
31
  /** Resources to `tilt trigger` (task 1.5: Tilt never cascades this on its own). */
27
32
  tiltTriggerNames: string[];
33
+ /**
34
+ * Whether to create/start the resource's container directly with
35
+ * `docker compose up --no-build` instead of `tilt trigger`. `tilt trigger`
36
+ * only queues the resource: with Tilt's build slots busy (e.g. 100+
37
+ * resources rebuilding after a re-vendor), a triggered resource waited
38
+ * behind them and the request timed out (observed live against
39
+ * tdk-erp-system, see tasks.md 7.2). server.ts falls back to `tilt trigger`
40
+ * when no image exists yet to start from.
41
+ */
42
+ useComposeStart: boolean;
28
43
  /**
29
44
  * Whether to also try Sablier's own Docker-level start for the resource.
30
45
  * Only meaningful when the resource's container already exists (D2/D3):
@@ -39,8 +54,9 @@ export interface WakeAction {
39
54
  */
40
55
  export function decideWakeAction(snapshot: WakeRequestSnapshot): WakeAction {
41
56
  const tiltTriggerNames: string[] = [];
57
+ const useComposeStart = snapshot.resourceState === "absent" && snapshot.hasComposeInvocation === true;
42
58
 
43
- if (snapshot.resourceState !== "running") {
59
+ if (snapshot.resourceState !== "running" && !useComposeStart) {
44
60
  tiltTriggerNames.push(snapshot.resourceContainerName);
45
61
  }
46
62
  for (const dep of snapshot.dependencies) {
@@ -51,7 +67,21 @@ export function decideWakeAction(snapshot: WakeRequestSnapshot): WakeAction {
51
67
 
52
68
  const useSablierStart = snapshot.resourceState === "exited" || snapshot.resourceState === "created";
53
69
 
54
- return { tiltTriggerNames, useSablierStart };
70
+ return { tiltTriggerNames, useComposeStart, useSablierStart };
71
+ }
72
+
73
+ /** Wait budget when the gateway starts an existing image itself (boot + healthcheck only). */
74
+ export const COMPOSE_START_TIMEOUT_MS = 180_000;
75
+ /**
76
+ * Wait budget when the resource has to go through `tilt trigger` (image build
77
+ * included). Kept under Bun.serve's 255s idleTimeout cap in server.ts.
78
+ */
79
+ export const TILT_TRIGGER_TIMEOUT_MS = 240_000;
80
+
81
+ /** The wait budget for a wake: an explicit X-Wake-Timeout-Ms wins, else by start path. */
82
+ export function wakeTimeoutMs(startedViaCompose: boolean, overrideMs?: number): number {
83
+ if (overrideMs && overrideMs > 0) return overrideMs;
84
+ return startedViaCompose ? COMPOSE_START_TIMEOUT_MS : TILT_TRIGGER_TIMEOUT_MS;
55
85
  }
56
86
 
57
87
  export interface WaitOutcome {
@@ -10,11 +10,10 @@
10
10
  * injects (set by the file-provider entry generated in
11
11
  * traefik_static_routes.star) rather than the path, so the original request
12
12
  * path/query reach the real backend unchanged once it's ready:
13
- * X-Wake-Resource Tilt/Compose resource name (for `tilt trigger`,
14
- * and the proxy target -- Compose's embedded DNS
15
- * resolves a service by this name too).
13
+ * X-Wake-Resource Tilt/Compose resource name (for `tilt trigger` and
14
+ * `docker compose up <service>`).
16
15
  * X-Wake-Container The actual Docker container name (for `docker
17
- * inspect` state/health -- NOT the same string as
16
+ * inspect` state/health and the proxy target -- NOT the same string as
18
17
  * the resource name; Compose derives it as
19
18
  * <project>-<service>-<replica>, confirmed empirically
20
19
  * against a live TDK container, e.g.
@@ -23,15 +22,28 @@
23
22
  * X-Wake-Deps Comma-separated "resourceName=containerName" pairs
24
23
  * for required dependencies (task 1.5: Tilt never
25
24
  * cascades a trigger to these on its own).
25
+ * X-Wake-Compose-Project The resource's Compose project (e.g. "analytics-autogenerated").
26
+ * X-Wake-Compose-Files Comma-separated absolute compose file paths, the
27
+ * same ones Tilt's `<resource>-run-only` resource
28
+ * passes. The project root is mounted into this
29
+ * container at the same absolute path, so relative
30
+ * paths inside those files resolve exactly as for Tilt.
31
+ * X-Wake-Env-File Absolute path of the project's .env (optional).
32
+ * X-Wake-Image The image the compose service declares
33
+ * (<service>_<resource>:dev).
26
34
  * X-Wake-Timeout-Ms, X-Wake-Proxy-Port, X-Wake-Session-Duration
27
35
  */
28
- import { decideWakeAction, evaluateReadiness, type ContainerState } from "./decide";
36
+ import {
37
+ decideWakeAction,
38
+ evaluateReadiness,
39
+ wakeTimeoutMs,
40
+ type ContainerState,
41
+ } from "./decide";
29
42
 
30
43
  const TILT_HOST = process.env.TILT_HOST || "host.docker.internal";
31
44
  const TILT_PORT = process.env.TILT_PORT || "10350";
32
45
  const SABLIER_URL = process.env.SABLIER_URL || "http://sablier:10000";
33
46
  const POLL_INTERVAL_MS = 1000;
34
- const DEFAULT_TIMEOUT_MS = 60_000;
35
47
 
36
48
  async function run(cmd: string[]): Promise<{ code: number; stdout: string; stderr: string }> {
37
49
  const proc = Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
@@ -66,7 +78,52 @@ async function isHealthy(name: string): Promise<boolean> {
66
78
  }
67
79
 
68
80
  async function tiltTrigger(name: string): Promise<void> {
69
- await run(["tilt", "trigger", name, "--host", TILT_HOST, "--port", TILT_PORT]);
81
+ const { code, stderr } = await run(["tilt", "trigger", name, "--host", TILT_HOST, "--port", TILT_PORT]);
82
+ console.log(`tilt trigger ${name}: ${code === 0 ? "queued" : `failed (${stderr.trim()})`}`);
83
+ }
84
+
85
+ interface ComposeInvocation {
86
+ project: string;
87
+ files: string[];
88
+ envFile: string;
89
+ image: string;
90
+ }
91
+
92
+ /**
93
+ * Make sure the compose service's declared image exists locally. Tilt tags its
94
+ * own builds `<repo>:tilt-<hash>` and injects that tag when it runs compose, so
95
+ * the declared `<repo>:dev` tag may be missing even though the resource was
96
+ * built; retag Tilt's newest build in that case. False when nothing was ever
97
+ * built -- only `tilt trigger` (which builds) can wake the resource then.
98
+ */
99
+ async function ensureImage(image: string): Promise<boolean> {
100
+ if ((await run(["docker", "image", "inspect", "--format", "{{.Id}}", image])).code === 0) return true;
101
+ const repo = image.split(":")[0];
102
+ // `docker images` lists newest first.
103
+ const { code, stdout } = await run(["docker", "images", repo, "--format", "{{.Tag}}"]);
104
+ if (code !== 0) return false;
105
+ const tiltTag = stdout.split("\n").map((t) => t.trim()).find((t) => t.startsWith("tilt-"));
106
+ if (!tiltTag) return false;
107
+ return (await run(["docker", "tag", `${repo}:${tiltTag}`, image])).code === 0;
108
+ }
109
+
110
+ /**
111
+ * Create and start the resource's container directly, with the same
112
+ * `docker compose ... up -d --no-build` its Tilt `-run-only` resource runs,
113
+ * bypassing Tilt's build queue (see decide.ts useComposeStart).
114
+ */
115
+ async function composeStart(compose: ComposeInvocation, service: string): Promise<boolean> {
116
+ if (!(await ensureImage(compose.image))) {
117
+ console.log(`compose start ${service}: no local image for ${compose.image}, falling back to tilt trigger`);
118
+ return false;
119
+ }
120
+ const args = ["docker", "compose", "-p", compose.project];
121
+ for (const file of compose.files) args.push("-f", file);
122
+ if (compose.envFile) args.push("--env-file", compose.envFile);
123
+ args.push("up", "-d", "--no-build", "--no-deps", service);
124
+ const { code, stderr } = await run(args);
125
+ console.log(`compose start ${service}: ${code === 0 ? "started" : `failed (${stderr.trim()})`}`);
126
+ return code === 0;
70
127
  }
71
128
 
72
129
  /**
@@ -110,14 +167,23 @@ function parseRequest(req: Request) {
110
167
  const resource = req.headers.get("X-Wake-Resource") || "";
111
168
  const container = req.headers.get("X-Wake-Container") || resource;
112
169
  const deps = parseDeps(req.headers.get("X-Wake-Deps") || "");
113
- const timeoutMs = Number(req.headers.get("X-Wake-Timeout-Ms")) || DEFAULT_TIMEOUT_MS;
170
+ const timeoutOverrideMs = Number(req.headers.get("X-Wake-Timeout-Ms")) || undefined;
114
171
  const proxyPort = req.headers.get("X-Wake-Proxy-Port") || "80";
115
172
  const sessionDuration = req.headers.get("X-Wake-Session-Duration") || "10m";
116
- return { resource, container, deps, timeoutMs, proxyPort, sessionDuration };
173
+ const composeFiles = (req.headers.get("X-Wake-Compose-Files") || "").split(",").filter(Boolean);
174
+ const compose: ComposeInvocation = {
175
+ project: req.headers.get("X-Wake-Compose-Project") || "",
176
+ files: composeFiles,
177
+ envFile: req.headers.get("X-Wake-Env-File") || "",
178
+ image: req.headers.get("X-Wake-Image") || "",
179
+ };
180
+ const hasCompose = Boolean(compose.project && compose.files.length && compose.image);
181
+ return { resource, container, deps, timeoutOverrideMs, proxyPort, sessionDuration, compose, hasCompose };
117
182
  }
118
183
 
119
184
  async function handleWake(req: Request): Promise<Response> {
120
- const { resource, container, deps, timeoutMs, proxyPort, sessionDuration } = parseRequest(req);
185
+ const { resource, container, deps, timeoutOverrideMs, proxyPort, sessionDuration, compose, hasCompose } =
186
+ parseRequest(req);
121
187
  if (!resource) {
122
188
  return new Response("wake gateway: missing X-Wake-Resource header (set by the generated Traefik static route)", { status: 400 });
123
189
  }
@@ -127,14 +193,32 @@ async function handleWake(req: Request): Promise<Response> {
127
193
  deps.map(async (dep) => ({ name: dep.resourceName, state: await containerState(dep.containerName) })),
128
194
  );
129
195
 
130
- const action = decideWakeAction({ resourceContainerName: resource, resourceState, dependencies });
196
+ const action = decideWakeAction({
197
+ resourceContainerName: resource,
198
+ resourceState,
199
+ dependencies,
200
+ hasComposeInvocation: hasCompose,
201
+ });
202
+ console.log(
203
+ `wake ${resource}: state=${resourceState} compose=${action.useComposeStart} ` +
204
+ `trigger=[${action.tiltTriggerNames.join(",")}] sablier=${action.useSablierStart}`,
205
+ );
206
+
207
+ // Dependencies (and the resource itself when it can't be started directly)
208
+ // go through tilt trigger (task 1.5: Tilt never cascades this on its own).
209
+ const tiltNames = [...action.tiltTriggerNames];
210
+ let startedViaCompose = false;
211
+ if (action.useComposeStart) {
212
+ startedViaCompose = await composeStart(compose, resource);
213
+ if (!startedViaCompose) tiltNames.unshift(resource);
214
+ }
215
+ const timeoutMs = wakeTimeoutMs(startedViaCompose, timeoutOverrideMs);
131
216
  const deadline = Date.now() + timeoutMs;
132
217
 
133
- // tilt trigger runs for everything cold (task 1.5: Tilt never cascades this
134
- // on its own); Sablier's blocking call, when applicable, both starts AND
135
- // waits for the resource itself in one round trip.
218
+ // Sablier's blocking call, when applicable, both starts AND waits for the
219
+ // resource itself in one round trip.
136
220
  const [, resourceReadyViaSablier] = await Promise.all([
137
- Promise.all(action.tiltTriggerNames.map(tiltTrigger)),
221
+ Promise.all(tiltNames.map(tiltTrigger)),
138
222
  action.useSablierStart
139
223
  ? sablierBlockingWait(resource, sessionDuration, timeoutMs)
140
224
  : Promise.resolve(false),
@@ -154,7 +238,7 @@ async function handleWake(req: Request): Promise<Response> {
154
238
  }
155
239
  const outcome = evaluateReadiness(requiredNames, healthy);
156
240
  if (outcome.ready) {
157
- return proxyThrough(req, resource, proxyPort);
241
+ return proxyThrough(req, container, proxyPort);
158
242
  }
159
243
  await Bun.sleep(POLL_INTERVAL_MS);
160
244
  }
@@ -176,9 +260,16 @@ const STRIPPED_REQUEST_HEADERS = new Set([
176
260
  "x-wake-timeout-ms",
177
261
  "x-wake-proxy-port",
178
262
  "x-wake-session-duration",
263
+ "x-wake-compose-project",
264
+ "x-wake-compose-files",
265
+ "x-wake-env-file",
266
+ "x-wake-image",
179
267
  ]);
180
268
 
181
- async function proxyThrough(req: Request, resource: string, port: string): Promise<Response> {
269
+ // Proxies to the container name rather than the Compose service name: every
270
+ // TDK project's resources share the traefik-public network, where a bare
271
+ // service name is only an alias and could collide across projects.
272
+ async function proxyThrough(req: Request, container: string, port: string): Promise<Response> {
182
273
  // The original path/query reach the gateway untouched (config travels via
183
274
  // X-Wake-* headers, not the path), so they're forwarded as-is.
184
275
  const target = new URL(req.url);
@@ -186,7 +277,7 @@ async function proxyThrough(req: Request, resource: string, port: string): Promi
186
277
  // Set hostname/port separately, not via `.host` -- confirmed empirically
187
278
  // that `target.host = "name:port"` silently keeps the URL's original port
188
279
  // when one is already present instead of replacing it.
189
- target.hostname = resource;
280
+ target.hostname = container;
190
281
  target.port = port;
191
282
 
192
283
  const headers = new Headers();
@@ -7,6 +7,7 @@
7
7
 
8
8
  load("../constants.star", "PlatformDockerConstants")
9
9
  load("../config/healthcheck.star", "compose_healthcheck_timing")
10
+ load("../networking/traefik_static_routes.star", "normalize_abs_path")
10
11
 
11
12
 
12
13
  def generate_standalone_traefik_compose():
@@ -91,7 +92,7 @@ services:
91
92
  - PORT={gateway_port}
92
93
  volumes:
93
94
  - /var/run/docker.sock:/var/run/docker.sock
94
- - {tilt_dev_dir}:/root/.tilt-dev:ro
95
+ - {tilt_dev_dir}:/root/.tilt-dev:ro{project_root_mount}
95
96
  networks:
96
97
  - traefik-public
97
98
 
@@ -108,4 +109,25 @@ networks:
108
109
  gateway_container=PlatformDockerConstants.TRAEFIK_WAKE_GATEWAY_CONTAINER,
109
110
  gateway_port=PlatformDockerConstants.TRAEFIK_WAKE_GATEWAY_PORT,
110
111
  tilt_dev_dir=os.environ.get("HOME", "") + "/.tilt-dev",
112
+ project_root_mount=_project_root_mount(),
111
113
  )
114
+
115
+
116
+ def _project_root_mount():
117
+ """Read-only mount of the project at its own absolute host path.
118
+
119
+ The gateway runs `docker compose up --no-build` for a deferred resource
120
+ itself (bypassing Tilt's build queue), using the compose files Tilt uses.
121
+ Compose resolves relative paths in them (env_file etc.) client-side, so
122
+ they must exist at the same absolute paths inside the gateway container.
123
+ Empty when TDK_PROJECT_ROOT is unset (the generated Tiltfile sets it).
124
+ """
125
+ root = project_root_abspath()
126
+ if not root:
127
+ return ""
128
+ return "\n - " + root + ":" + root + ":ro"
129
+
130
+
131
+ def project_root_abspath():
132
+ """Absolute, normalized project root (TDK_PROJECT_ROOT), or "" when unset."""
133
+ return normalize_abs_path(os.environ.get("TDK_PROJECT_ROOT", ""))
@@ -13,10 +13,15 @@
13
13
  # backend was healthy).
14
14
  #
15
15
  # Config for a request travels to the gateway via headers (X-Wake-*), not
16
- # the path/query, so the original request path reaches the real backend
17
- # unchanged once the resource is ready (see wake-gateway/server.ts).
16
+ # the path/query. The route still strips the same `apiPath` prefix the real
17
+ # Docker-label `-project` router strips (get_backend_traefik_labels) before
18
+ # forwarding -- confirmed live against tdk-erp-system (task 7.2) that a
19
+ # backend's app only serves its bare health path (e.g. `/health`), not the
20
+ # full `/api/{name}/health` the public route is matched on; without this, the
21
+ # gateway's own proxy-through would forward the unstripped path once healthy
22
+ # and the app would 404 on it, even though the wake itself succeeded.
18
23
 
19
- load("./traefik_helpers.star", "project_backend_rule", "build_entrypoints")
24
+ load("./traefik_helpers.star", "project_backend_rule", "cli_api_path", "build_entrypoints")
20
25
  load("./traefik_constants.star",
21
26
  "TRAEFIK_BACKEND_ENABLE_HTTP",
22
27
  "TRAEFIK_BACKEND_ENABLE_HTTPS",
@@ -25,6 +30,29 @@ load("./traefik_constants.star",
25
30
  STATIC_ROUTE_PRIORITY = 1
26
31
 
27
32
 
33
+ def normalize_abs_path(path):
34
+ """Absolute path with `.`/`..` segments collapsed, or "" for "".
35
+
36
+ Tilt's os.path.abspath does not collapse `..` (confirmed:
37
+ abspath("/a/.tdk/.tdk-out/../..") comes back unchanged), and the
38
+ generated Tiltfile's project root is exactly that shape
39
+ (config.main_dir + "/../.."). The gateway's project mount and the compose
40
+ paths in its route headers must match byte-for-byte, so both go through here.
41
+ """
42
+ if not path:
43
+ return ""
44
+ parts = []
45
+ for segment in os.path.abspath(path).split("/"):
46
+ if segment == "" or segment == ".":
47
+ continue
48
+ if segment == "..":
49
+ if parts:
50
+ parts.pop()
51
+ continue
52
+ parts.append(segment)
53
+ return "/" + "/".join(parts)
54
+
55
+
28
56
  def _container_name(compose_project_name, res_name):
29
57
  """Docker Compose's own naming convention: <project>-<service>-<replica>.
30
58
 
@@ -43,6 +71,9 @@ def generate_static_wake_route(
43
71
  gateway_host,
44
72
  gateway_port,
45
73
  dependencies=None,
74
+ compose_files=None,
75
+ env_file="",
76
+ image="",
46
77
  ):
47
78
  """YAML for one resource's Traefik file-provider dynamic-config entry.
48
79
 
@@ -51,21 +82,36 @@ def generate_static_wake_route(
51
82
  different compose project than the resource itself, so the project name
52
83
  travels alongside the resource name rather than being assumed shared.
53
84
 
85
+ `compose_files` (absolute paths), `env_file` and `image` let the gateway
86
+ create the container itself with `docker compose up --no-build` when the
87
+ image already exists, instead of waiting in Tilt's build queue behind
88
+ `tilt trigger`. Omitted headers make the gateway use `tilt trigger` only.
89
+
54
90
  Written to the directory PlatformDockerConstants.TRAEFIK_DYNAMIC_DIR_REL
55
91
  mounts into the standalone Traefik container (traefik_standalone.star).
56
92
  """
57
93
  dependencies = dependencies or []
58
94
 
59
95
  rule = project_backend_rule(manifest, resource_entry_name)
96
+ api_path = cli_api_path(resource_entry_name, manifest)
60
97
  entrypoints = build_entrypoints(TRAEFIK_BACKEND_ENABLE_HTTP, TRAEFIK_BACKEND_ENABLE_HTTPS)
61
98
  router_name = resource_entry_name + "-wake"
62
- middleware_name = resource_entry_name + "-wake-headers"
99
+ headers_middleware_name = resource_entry_name + "-wake-headers"
100
+ strip_middleware_name = resource_entry_name + "-wake-strip"
63
101
  service_name = resource_entry_name + "-wake-gateway"
64
102
  container_name = _container_name(compose_project_name, resource_entry_name)
65
103
  deps_header = ",".join([
66
104
  dep_res_name + "=" + _container_name(dep_project_name, dep_res_name)
67
105
  for dep_res_name, dep_project_name in dependencies
68
106
  ])
107
+ compose_headers = ""
108
+ if compose_files and image:
109
+ compose_headers = (
110
+ '\n X-Wake-Compose-Project: "' + compose_project_name + '"'
111
+ + '\n X-Wake-Compose-Files: "' + ",".join(compose_files) + '"'
112
+ + '\n X-Wake-Env-File: "' + env_file + '"'
113
+ + '\n X-Wake-Image: "' + image + '"'
114
+ )
69
115
 
70
116
  return """# SYSTEM-GENERATED - DO NOT EDIT (sablier.deferStart static wake route)
71
117
  http:
@@ -76,16 +122,21 @@ http:
76
122
  - {entrypoint}
77
123
  service: {service_name}
78
124
  middlewares:
79
- - {middleware_name}
125
+ - {strip_middleware_name}
126
+ - {headers_middleware_name}
80
127
  priority: {priority}
81
128
  middlewares:
82
- {middleware_name}:
129
+ {strip_middleware_name}:
130
+ stripPrefix:
131
+ prefixes:
132
+ - "{api_path}"
133
+ {headers_middleware_name}:
83
134
  headers:
84
135
  customRequestHeaders:
85
136
  X-Wake-Resource: "{resource_entry_name}"
86
137
  X-Wake-Container: "{container_name}"
87
138
  X-Wake-Deps: "{deps_header}"
88
- X-Wake-Proxy-Port: "{internal_port}"
139
+ X-Wake-Proxy-Port: "{internal_port}"{compose_headers}
89
140
  services:
90
141
  {service_name}:
91
142
  loadBalancer:
@@ -96,12 +147,15 @@ http:
96
147
  rule=rule,
97
148
  entrypoint=entrypoints.split(",")[0],
98
149
  service_name=service_name,
99
- middleware_name=middleware_name,
150
+ strip_middleware_name=strip_middleware_name,
151
+ headers_middleware_name=headers_middleware_name,
152
+ api_path=api_path,
100
153
  priority=STATIC_ROUTE_PRIORITY,
101
154
  resource_entry_name=resource_entry_name,
102
155
  container_name=container_name,
103
156
  deps_header=deps_header,
104
157
  internal_port=internal_port,
158
+ compose_headers=compose_headers,
105
159
  gateway_host=gateway_host,
106
160
  gateway_port=gateway_port,
107
161
  )
@@ -10,7 +10,7 @@ OUTPUT = struct(AUTOGENERATED_FOLDER = ".autogenerated")
10
10
  load('../../../platform/docker/index.star', 'Docker')
11
11
  load('../../../platform/docker/constants.star', 'PlatformDockerConstants')
12
12
  load('../../../tilt/manifest/constants.star', 'BASE_PORT_FRONTEND', 'BASE_PORT_BACKEND')
13
- load('../../../platform/docker/networking/traefik_static_routes.star', 'generate_static_wake_route')
13
+ load('../../../platform/docker/networking/traefik_static_routes.star', 'generate_static_wake_route', 'normalize_abs_path')
14
14
  load('../../../platform/docker/networking/sablier_container_cycle.star', 'sablier_middleware_suffix')
15
15
 
16
16
  load('./builders/typescript.star', 'TypescriptBuilders')
@@ -213,9 +213,38 @@ def _build_live_update_rules(res_path, full_res_path, syncs):
213
213
  return live_update_rules
214
214
 
215
215
 
216
+ def _app_image_name(resource_name, res_name):
217
+ """The image ref docker_build builds and the app compose service declares."""
218
+ return resource_name + '_' + res_name + ':dev'
219
+
220
+
221
+ def _wake_compose_invocation(project_root, resource_path, res_name, resource_name):
222
+ """Compose files, env file and image for the wake gateway's direct start.
223
+
224
+ The same files/--env-file the `-run-only` resource passes, as absolute
225
+ normalized paths: the gateway mounts the project at the same normalized
226
+ root (traefik_standalone.star), so these resolve identically there.
227
+ Returns None without a project root (paths would be relative to Tilt's cwd).
228
+ """
229
+ if not project_root:
230
+ return None
231
+ root = normalize_abs_path(project_root)
232
+ compose_dir = root + '/' + resource_path + '/' + OUTPUT.AUTOGENERATED_FOLDER
233
+ files = [compose_dir + '/docker-compose.app.autogenerated.yml']
234
+ port_override = compose_dir + '/docker-compose.port-override.yml'
235
+ if os.path.exists(port_override):
236
+ files.append(port_override)
237
+ env_file = root + '/.env'
238
+ return {
239
+ 'compose_files': files,
240
+ 'env_file': env_file if os.path.exists(env_file) else '',
241
+ 'image': _app_image_name(resource_name, res_name),
242
+ }
243
+
244
+
216
245
  def _register_docker_build(config, live_update_rules):
217
246
  """Register docker_build with live_update for hot-reload support."""
218
- image_name = config['resource_name'] + '_' + config['res_name'] + ':dev'
247
+ image_name = _app_image_name(config['resource_name'], config['res_name'])
219
248
  project_root = config.get('project_root', '')
220
249
  # Use custom dockerfile from config if specified, otherwise use autogenerated default
221
250
  # The res_path is relative to project_root (e.g., 'services/product/...')
@@ -478,6 +507,9 @@ def register_compose_resources(resource_config, ctx, runtime_flags, manifest_sta
478
507
  if resource_defers_start(manifest) and write_file:
479
508
  internal_port = res.get('port', manifest.get('port', BASE_PORT_BACKEND))
480
509
  static_route_path = PlatformDockerConstants.TRAEFIK_DYNAMIC_DIR_REL + '/' + res['name'] + '.yml'
510
+ compose_invocation = _wake_compose_invocation(
511
+ ctx.get('project_root', ''), resource_path, res['name'], resource_name,
512
+ ) or {}
481
513
  write_file(static_route_path, generate_static_wake_route(
482
514
  resource_entry_name=res['name'],
483
515
  compose_project_name=compose_project_name,
@@ -486,6 +518,9 @@ def register_compose_resources(resource_config, ctx, runtime_flags, manifest_sta
486
518
  gateway_host=PlatformDockerConstants.TRAEFIK_WAKE_GATEWAY_CONTAINER,
487
519
  gateway_port=PlatformDockerConstants.TRAEFIK_WAKE_GATEWAY_PORT,
488
520
  dependencies=resolve_defer_start_dependencies(manifest, resource_manifests, all_services_map),
521
+ compose_files=compose_invocation.get('compose_files'),
522
+ env_file=compose_invocation.get('env_file', ''),
523
+ image=compose_invocation.get('image', ''),
489
524
  ))
490
525
 
491
526
  resource_entries.append(entry)