@tdk-landscape/tdk-cli-core 1.3.49 → 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/.claude/skills/layer-autoresearch/SKILL.md +48 -0
- package/.claude/skills/tdk-troubleshoot/SKILL.md +222 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +40 -0
- package/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +18 -0
- package/.github/dependabot.yml +33 -0
- package/.github/pull_request_template.md +5 -0
- package/.github/workflows/ci.yml +187 -0
- package/.github/workflows/release-binaries.yml +19 -9
- package/.github/zizmor.yml +9 -0
- package/CODE_OF_CONDUCT.md +9 -0
- package/CONTRIBUTING.md +42 -0
- package/LICENSE +21 -0
- package/README.md +151 -0
- package/SECURITY.md +17 -0
- package/benchmarks/results/container-scale-2026-09-25T20-54-49-736Z.json +45 -0
- package/benchmarks/results/container-scale-2026-09-25T21-01-20-540Z.json +150 -0
- package/benchmarks/results/container-scale-2026-09-25T21-15-01-441Z.json +73 -0
- package/benchmarks/results/container-scale-2026-09-25T22-09-01-630Z.json +77 -0
- package/benchmarks/results/container-scale-2026-09-25T22-18-28-371Z.json +202 -0
- package/cli/README.md +25 -0
- package/cli/biome.json +1 -1
- package/cli/dist/cli.d.ts +3 -0
- package/cli/dist/cli.d.ts.map +1 -0
- package/cli/dist/cli.js +64 -0
- package/cli/dist/cli.js.map +1 -0
- package/cli/dist/commands/completion.d.ts +3 -0
- package/cli/dist/commands/completion.d.ts.map +1 -0
- package/cli/dist/commands/completion.js +294 -0
- package/cli/dist/commands/completion.js.map +1 -0
- package/cli/dist/commands/config.d.ts +3 -0
- package/cli/dist/commands/config.d.ts.map +1 -0
- package/cli/dist/commands/config.js +231 -0
- package/cli/dist/commands/config.js.map +1 -0
- package/cli/dist/commands/doctor.d.ts +19 -0
- package/cli/dist/commands/doctor.d.ts.map +1 -0
- package/cli/dist/commands/doctor.js +745 -0
- package/cli/dist/commands/doctor.js.map +1 -0
- package/cli/dist/commands/down.d.ts +3 -0
- package/cli/dist/commands/down.d.ts.map +1 -0
- package/cli/dist/commands/down.js +32 -0
- package/cli/dist/commands/down.js.map +1 -0
- package/cli/dist/commands/help.d.ts +2 -0
- package/cli/dist/commands/help.d.ts.map +1 -0
- package/cli/dist/commands/help.js +138 -0
- package/cli/dist/commands/help.js.map +1 -0
- package/cli/dist/commands/networks.d.ts +3 -0
- package/cli/dist/commands/networks.d.ts.map +1 -0
- package/cli/dist/commands/networks.js +260 -0
- package/cli/dist/commands/networks.js.map +1 -0
- package/cli/dist/commands/project.d.ts +3 -0
- package/cli/dist/commands/project.d.ts.map +1 -0
- package/cli/dist/commands/project.js +370 -0
- package/cli/dist/commands/project.js.map +1 -0
- package/cli/dist/commands/projects.d.ts +3 -0
- package/cli/dist/commands/projects.d.ts.map +1 -0
- package/cli/dist/commands/projects.js +71 -0
- package/cli/dist/commands/projects.js.map +1 -0
- package/cli/dist/commands/resource.d.ts +69 -0
- package/cli/dist/commands/resource.d.ts.map +1 -0
- package/cli/dist/commands/resource.js +579 -0
- package/cli/dist/commands/resource.js.map +1 -0
- package/cli/dist/commands/resources.d.ts +3 -0
- package/cli/dist/commands/resources.d.ts.map +1 -0
- package/cli/dist/commands/resources.js +73 -0
- package/cli/dist/commands/resources.js.map +1 -0
- package/cli/dist/commands/stack.d.ts +3 -0
- package/cli/dist/commands/stack.d.ts.map +1 -0
- package/cli/dist/commands/stack.js +97 -0
- package/cli/dist/commands/stack.js.map +1 -0
- package/cli/dist/commands/stacks.d.ts +3 -0
- package/cli/dist/commands/stacks.d.ts.map +1 -0
- package/cli/dist/commands/stacks.js +43 -0
- package/cli/dist/commands/stacks.js.map +1 -0
- package/cli/dist/commands/status.d.ts +3 -0
- package/cli/dist/commands/status.d.ts.map +1 -0
- package/cli/dist/commands/status.js +71 -0
- package/cli/dist/commands/status.js.map +1 -0
- package/cli/dist/commands/ui.d.ts +3 -0
- package/cli/dist/commands/ui.d.ts.map +1 -0
- package/cli/dist/commands/ui.js +445 -0
- package/cli/dist/commands/ui.js.map +1 -0
- package/cli/dist/commands/up.d.ts +3 -0
- package/cli/dist/commands/up.d.ts.map +1 -0
- package/cli/dist/commands/up.js +124 -0
- package/cli/dist/commands/up.js.map +1 -0
- package/cli/dist/commands/upgrade.d.ts +11 -0
- package/cli/dist/commands/upgrade.d.ts.map +1 -0
- package/cli/dist/commands/upgrade.js +496 -0
- package/cli/dist/commands/upgrade.js.map +1 -0
- package/cli/dist/commands/version.d.ts +3 -0
- package/cli/dist/commands/version.d.ts.map +1 -0
- package/cli/dist/commands/version.js +9 -0
- package/cli/dist/commands/version.js.map +1 -0
- package/cli/dist/components/Accessible.d.ts +8 -0
- package/cli/dist/components/Accessible.d.ts.map +1 -0
- package/cli/dist/components/Accessible.js +10 -0
- package/cli/dist/components/Accessible.js.map +1 -0
- package/cli/dist/components/BaseTooltip.d.ts +4 -0
- package/cli/dist/components/BaseTooltip.d.ts.map +1 -0
- package/cli/dist/components/BaseTooltip.js +28 -0
- package/cli/dist/components/BaseTooltip.js.map +1 -0
- package/cli/dist/components/DetailPanel.d.ts +4 -0
- package/cli/dist/components/DetailPanel.d.ts.map +1 -0
- package/cli/dist/components/DetailPanel.js +18 -0
- package/cli/dist/components/DetailPanel.js.map +1 -0
- package/cli/dist/components/FileTree.d.ts +4 -0
- package/cli/dist/components/FileTree.d.ts.map +1 -0
- package/cli/dist/components/FileTree.js +47 -0
- package/cli/dist/components/FileTree.js.map +1 -0
- package/cli/dist/components/ResourceSelectInput.d.ts +4 -0
- package/cli/dist/components/ResourceSelectInput.d.ts.map +1 -0
- package/cli/dist/components/ResourceSelectInput.js +7 -0
- package/cli/dist/components/ResourceSelectInput.js.map +1 -0
- package/cli/dist/components/ResourceTable.d.ts +4 -0
- package/cli/dist/components/ResourceTable.d.ts.map +1 -0
- package/cli/dist/components/ResourceTable.js +18 -0
- package/cli/dist/components/ResourceTable.js.map +1 -0
- package/cli/dist/components/TabBar.d.ts +4 -0
- package/cli/dist/components/TabBar.d.ts.map +1 -0
- package/cli/dist/components/TabBar.js +17 -0
- package/cli/dist/components/TabBar.js.map +1 -0
- package/cli/dist/components/Tooltip.d.ts +12 -0
- package/cli/dist/components/Tooltip.d.ts.map +1 -0
- package/cli/dist/components/Tooltip.js +12 -0
- package/cli/dist/components/Tooltip.js.map +1 -0
- package/cli/dist/components/index.d.ts +8 -0
- package/cli/dist/components/index.d.ts.map +1 -0
- package/cli/dist/components/index.js +8 -0
- package/cli/dist/components/index.js.map +1 -0
- package/cli/dist/config/platform-standards.d.ts +159 -0
- package/cli/dist/config/platform-standards.d.ts.map +1 -0
- package/cli/dist/config/platform-standards.js +181 -0
- package/cli/dist/config/platform-standards.js.map +1 -0
- package/cli/dist/generator/extension-fetch.d.ts +25 -0
- package/cli/dist/generator/extension-fetch.d.ts.map +1 -0
- package/cli/dist/generator/extension-fetch.js +259 -0
- package/cli/dist/generator/extension-fetch.js.map +1 -0
- package/cli/dist/generator/template-engine.d.ts +62 -0
- package/cli/dist/generator/template-engine.d.ts.map +1 -0
- package/cli/dist/generator/template-engine.js +623 -0
- package/cli/dist/generator/template-engine.js.map +1 -0
- package/cli/dist/index.d.ts +11 -0
- package/cli/dist/index.d.ts.map +1 -0
- package/cli/dist/index.js +11 -0
- package/cli/dist/index.js.map +1 -0
- package/cli/dist/types/index.d.ts +257 -0
- package/cli/dist/types/index.d.ts.map +1 -0
- package/cli/dist/types/index.js +17 -0
- package/cli/dist/types/index.js.map +1 -0
- package/cli/dist/utils/cache.d.ts +19 -0
- package/cli/dist/utils/cache.d.ts.map +1 -0
- package/cli/dist/utils/cache.js +64 -0
- package/cli/dist/utils/cache.js.map +1 -0
- package/cli/dist/utils/command-helpers.d.ts +17 -0
- package/cli/dist/utils/command-helpers.d.ts.map +1 -0
- package/cli/dist/utils/command-helpers.js +41 -0
- package/cli/dist/utils/command-helpers.js.map +1 -0
- package/cli/dist/utils/constants.d.ts +53 -0
- package/cli/dist/utils/constants.d.ts.map +1 -0
- package/cli/dist/utils/constants.js +115 -0
- package/cli/dist/utils/constants.js.map +1 -0
- package/cli/dist/utils/discovery-context.d.ts +4 -0
- package/cli/dist/utils/discovery-context.d.ts.map +1 -0
- package/cli/dist/utils/discovery-context.js +39 -0
- package/cli/dist/utils/discovery-context.js.map +1 -0
- package/cli/dist/utils/docker.d.ts +4 -0
- package/cli/dist/utils/docker.d.ts.map +1 -0
- package/cli/dist/utils/docker.js +19 -0
- package/cli/dist/utils/docker.js.map +1 -0
- package/cli/dist/utils/doctor-runtime.d.ts +81 -0
- package/cli/dist/utils/doctor-runtime.d.ts.map +1 -0
- package/cli/dist/utils/doctor-runtime.js +498 -0
- package/cli/dist/utils/doctor-runtime.js.map +1 -0
- package/cli/dist/utils/env-validator.d.ts +15 -0
- package/cli/dist/utils/env-validator.d.ts.map +1 -0
- package/cli/dist/utils/env-validator.js +109 -0
- package/cli/dist/utils/env-validator.js.map +1 -0
- package/cli/dist/utils/errors.d.ts +31 -0
- package/cli/dist/utils/errors.d.ts.map +1 -0
- package/cli/dist/utils/errors.js +140 -0
- package/cli/dist/utils/errors.js.map +1 -0
- package/cli/dist/utils/file-helpers.d.ts +14 -0
- package/cli/dist/utils/file-helpers.d.ts.map +1 -0
- package/cli/dist/utils/file-helpers.js +45 -0
- package/cli/dist/utils/file-helpers.js.map +1 -0
- package/cli/dist/utils/formatting.d.ts +30 -0
- package/cli/dist/utils/formatting.d.ts.map +1 -0
- package/cli/dist/utils/formatting.js +164 -0
- package/cli/dist/utils/formatting.js.map +1 -0
- package/cli/dist/utils/paths.d.ts +3 -0
- package/cli/dist/utils/paths.d.ts.map +1 -0
- package/cli/dist/utils/paths.js +41 -0
- package/cli/dist/utils/paths.js.map +1 -0
- package/cli/dist/utils/port-assignment.d.ts +6 -0
- package/cli/dist/utils/port-assignment.d.ts.map +1 -0
- package/cli/dist/utils/port-assignment.js +81 -0
- package/cli/dist/utils/port-assignment.js.map +1 -0
- package/cli/dist/utils/project-features.d.ts +23 -0
- package/cli/dist/utils/project-features.d.ts.map +1 -0
- package/cli/dist/utils/project-features.js +98 -0
- package/cli/dist/utils/project-features.js.map +1 -0
- package/cli/dist/utils/project-templates.d.ts +7 -0
- package/cli/dist/utils/project-templates.d.ts.map +1 -0
- package/cli/dist/utils/project-templates.js +24 -0
- package/cli/dist/utils/project-templates.js.map +1 -0
- package/cli/dist/utils/prompt.d.ts +28 -0
- package/cli/dist/utils/prompt.d.ts.map +1 -0
- package/cli/dist/utils/prompt.js +79 -0
- package/cli/dist/utils/prompt.js.map +1 -0
- package/cli/dist/utils/resource-features.d.ts +29 -0
- package/cli/dist/utils/resource-features.d.ts.map +1 -0
- package/cli/dist/utils/resource-features.js +175 -0
- package/cli/dist/utils/resource-features.js.map +1 -0
- package/cli/dist/utils/service-urls.d.ts +30 -0
- package/cli/dist/utils/service-urls.d.ts.map +1 -0
- package/cli/dist/utils/service-urls.js +93 -0
- package/cli/dist/utils/service-urls.js.map +1 -0
- package/cli/dist/utils/services.d.ts +18 -0
- package/cli/dist/utils/services.d.ts.map +1 -0
- package/cli/dist/utils/services.js +292 -0
- package/cli/dist/utils/services.js.map +1 -0
- package/cli/dist/utils/stack-features.d.ts +22 -0
- package/cli/dist/utils/stack-features.d.ts.map +1 -0
- package/cli/dist/utils/stack-features.js +13 -0
- package/cli/dist/utils/stack-features.js.map +1 -0
- package/cli/dist/utils/tar.d.ts +7 -0
- package/cli/dist/utils/tar.d.ts.map +1 -0
- package/cli/dist/utils/tar.js +21 -0
- package/cli/dist/utils/tar.js.map +1 -0
- package/cli/dist/utils/tilt.d.ts +20 -0
- package/cli/dist/utils/tilt.d.ts.map +1 -0
- package/cli/dist/utils/tilt.js +95 -0
- package/cli/dist/utils/tilt.js.map +1 -0
- package/cli/dist/utils/validation.d.ts +16 -0
- package/cli/dist/utils/validation.d.ts.map +1 -0
- package/cli/dist/utils/validation.js +58 -0
- package/cli/dist/utils/validation.js.map +1 -0
- package/cli/package.json +11 -12
- package/cli/src/commands/__tests__/always-enabled-infra.test.ts +2 -8
- package/cli/src/commands/__tests__/doctor.test.ts +181 -8
- package/cli/src/commands/__tests__/phases-model.test.ts +2 -4
- package/cli/src/commands/__tests__/project-resource-features-e2e.test.ts +4 -24
- package/cli/src/commands/__tests__/saas-starter-integration.test.ts +8 -10
- package/cli/src/commands/__tests__/stack-features-e2e.test.ts +10 -4
- package/cli/src/commands/__tests__/upgrade.test.ts +23 -13
- package/cli/src/commands/doctor.ts +136 -0
- package/cli/src/commands/help.ts +13 -3
- package/cli/src/commands/project.ts +11 -14
- package/cli/src/commands/projects.ts +2 -2
- package/cli/src/commands/up.ts +13 -4
- package/cli/src/commands/upgrade.ts +11 -4
- package/cli/src/config/platform-standards.test.ts +6 -6
- package/cli/src/generator/__tests__/extension-fetch.test.ts +36 -14
- package/cli/src/generator/extension-fetch.ts +12 -5
- package/cli/src/generator/template-engine.ts +19 -21
- package/cli/src/types/index.ts +2 -0
- package/cli/src/utils/__tests__/tilt.test.ts +29 -0
- package/cli/src/utils/constants.ts +4 -0
- package/cli/src/utils/doctor-runtime.ts +67 -27
- package/cli/src/utils/env-validator.ts +2 -2
- package/cli/src/utils/project-templates.ts +2 -1
- package/cli/src/utils/prompt.ts +2 -4
- package/cli/src/utils/resource-features.ts +10 -4
- package/cli/src/utils/services.ts +2 -1
- package/cli/src/utils/tilt.ts +20 -0
- package/docs/demo.svg +217 -0
- package/engine/assets/docker/wake-gateway/Dockerfile +27 -0
- package/engine/assets/docker/wake-gateway/decide.test.ts +133 -0
- package/engine/assets/docker/wake-gateway/decide.ts +105 -0
- package/engine/assets/docker/wake-gateway/server.ts +311 -0
- package/engine/topologies/platform/docker/build/golden_image_dockerfile.star +13 -23
- package/engine/topologies/platform/docker/compose/compose.star +7 -14
- package/engine/topologies/platform/docker/compose/compose_helpers.star +1 -1
- package/engine/topologies/platform/docker/compose/traefik_standalone.star +66 -6
- package/engine/topologies/platform/docker/config/healthcheck.star +56 -0
- package/engine/topologies/platform/docker/constants.star +10 -0
- package/engine/topologies/platform/docker/layers/l1_base_layers.star +1 -1
- package/engine/topologies/platform/docker/layers/l4_runtime_layers.star +55 -20
- package/engine/topologies/platform/docker/networking/traefik_static_routes.star +161 -0
- package/engine/topologies/tilt/generators/typescript/backend_tsconfig.star +2 -2
- package/engine/topologies/tilt/generators/typescript/frontend_tsconfig.star +1 -1
- package/engine/topologies/tilt/manifest/schema.star +6 -0
- package/engine/topologies/tilt/manifest/validator.star +16 -0
- package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +143 -9
- package/engine/topologies/tilt/resources/orchestrator/builders/typescript.star +12 -6
- package/engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star +2 -1
- package/knip.json +1 -1
- package/openspec/changes/optimize-runtime-docker-layers/.openspec.yaml +3 -0
- package/openspec/changes/optimize-runtime-docker-layers/design.md +81 -0
- package/openspec/changes/optimize-runtime-docker-layers/proposal.md +44 -0
- package/openspec/changes/optimize-runtime-docker-layers/specs/runtime-container-footprint/spec.md +79 -0
- package/openspec/changes/optimize-runtime-docker-layers/tasks.md +36 -0
- package/openspec/changes/prioritized-cold-start/.openspec.yaml +4 -0
- package/openspec/changes/prioritized-cold-start/design.md +133 -0
- package/openspec/changes/prioritized-cold-start/proposal.md +41 -0
- package/openspec/changes/prioritized-cold-start/specs/on-demand-resource-startup/spec.md +92 -0
- package/openspec/changes/prioritized-cold-start/tasks.md +56 -0
- package/openspec/config.yaml +1 -0
- package/package.json +35 -5
- package/scripts/autoresearch/layer-eval.sh +65 -0
- package/scripts/benchmark/README.md +25 -0
- package/scripts/benchmark/container-scale.ts +503 -0
- package/scripts/benchmark/pre-release-gate.sh +9 -0
- package/scripts/measure-idle-footprint.sh +58 -0
- package/scripts/record-demo.sh +43 -0
- package/tests/tilt-engine/test_runtime_container_footprint.py +162 -0
- package/tests/tilt-engine/test_sablier_defer_start.py +196 -0
- package/tests/tilt-engine/test_standalone_traefik.py +52 -0
- package/tests/tilt-engine/test_traefik_static_wake_route.py +187 -0
- package/bun.lock +0 -538
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Wake gateway (see openspec/changes/prioritized-cold-start): fronts every
|
|
2
|
+
# `sablier.deferStart` resource's Traefik static route, triggers cold
|
|
3
|
+
# containers via `tilt trigger` and/or Sablier, then holds the request until
|
|
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
|
|
7
|
+
# documented `tilt trigger` command rather than depending on Tilt's
|
|
8
|
+
# undocumented internal HTTP API).
|
|
9
|
+
|
|
10
|
+
FROM oven/bun:1.3.11-alpine AS base
|
|
11
|
+
|
|
12
|
+
# Pin to the same Tilt version this workspace runs (v0.37.7) so `tilt trigger`
|
|
13
|
+
# talks the same protocol as the real daemon it's connecting to. Installed
|
|
14
|
+
# directly (not via tilt-dev's install.sh, which shells out to `sudo` -- not
|
|
15
|
+
# present in this minimal image, and unnecessary since we're already root).
|
|
16
|
+
ARG TILT_VERSION=0.37.7
|
|
17
|
+
RUN apk add --no-cache docker-cli docker-cli-compose curl gcompat \
|
|
18
|
+
&& arch="$(uname -m)" \
|
|
19
|
+
&& case "$arch" in aarch64) tilt_arch=arm64 ;; x86_64) tilt_arch=x86_64 ;; *) echo "unsupported arch: $arch" >&2; exit 1 ;; esac \
|
|
20
|
+
&& curl -fsSL "https://github.com/tilt-dev/tilt/releases/download/v${TILT_VERSION}/tilt.${TILT_VERSION}.linux.${tilt_arch}.tar.gz" \
|
|
21
|
+
| tar -xz -C /usr/local/bin tilt
|
|
22
|
+
|
|
23
|
+
WORKDIR /app
|
|
24
|
+
COPY decide.ts server.ts ./
|
|
25
|
+
|
|
26
|
+
EXPOSE 9999
|
|
27
|
+
CMD ["bun", "run", "server.ts"]
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
COMPOSE_START_TIMEOUT_MS,
|
|
4
|
+
TILT_TRIGGER_TIMEOUT_MS,
|
|
5
|
+
decideWakeAction,
|
|
6
|
+
evaluateReadiness,
|
|
7
|
+
wakeTimeoutMs,
|
|
8
|
+
} from "./decide";
|
|
9
|
+
|
|
10
|
+
describe("decideWakeAction", () => {
|
|
11
|
+
test("never-created resource: tilt trigger only, no Sablier start", () => {
|
|
12
|
+
const action = decideWakeAction({
|
|
13
|
+
resourceContainerName: "data-governance-api",
|
|
14
|
+
resourceState: "absent",
|
|
15
|
+
dependencies: [],
|
|
16
|
+
});
|
|
17
|
+
expect(action.tiltTriggerNames).toEqual(["data-governance-api"]);
|
|
18
|
+
expect(action.useSablierStart).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("previously-run, now-exited resource: both tilt trigger and Sablier start", () => {
|
|
22
|
+
const action = decideWakeAction({
|
|
23
|
+
resourceContainerName: "data-governance-api",
|
|
24
|
+
resourceState: "exited",
|
|
25
|
+
dependencies: [],
|
|
26
|
+
});
|
|
27
|
+
expect(action.tiltTriggerNames).toEqual(["data-governance-api"]);
|
|
28
|
+
expect(action.useSablierStart).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("created-but-not-started resource: both paths apply", () => {
|
|
32
|
+
const action = decideWakeAction({
|
|
33
|
+
resourceContainerName: "data-governance-api",
|
|
34
|
+
resourceState: "created",
|
|
35
|
+
dependencies: [],
|
|
36
|
+
});
|
|
37
|
+
expect(action.useSablierStart).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("already running: no trigger needed", () => {
|
|
41
|
+
const action = decideWakeAction({
|
|
42
|
+
resourceContainerName: "data-governance-api",
|
|
43
|
+
resourceState: "running",
|
|
44
|
+
dependencies: [],
|
|
45
|
+
});
|
|
46
|
+
expect(action.tiltTriggerNames).toEqual([]);
|
|
47
|
+
expect(action.useSablierStart).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("cold dependency is triggered alongside the resource (task 1.5 finding)", () => {
|
|
51
|
+
const action = decideWakeAction({
|
|
52
|
+
resourceContainerName: "data-governance-api",
|
|
53
|
+
resourceState: "absent",
|
|
54
|
+
dependencies: [
|
|
55
|
+
{ name: "postgres", state: "running" },
|
|
56
|
+
{ name: "data-governance-api-db-migrator", state: "absent" },
|
|
57
|
+
],
|
|
58
|
+
});
|
|
59
|
+
expect(action.tiltTriggerNames).toEqual([
|
|
60
|
+
"data-governance-api",
|
|
61
|
+
"data-governance-api-db-migrator",
|
|
62
|
+
]);
|
|
63
|
+
});
|
|
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
|
+
|
|
90
|
+
test("running dependency is not re-triggered", () => {
|
|
91
|
+
const action = decideWakeAction({
|
|
92
|
+
resourceContainerName: "data-governance-api",
|
|
93
|
+
resourceState: "running",
|
|
94
|
+
dependencies: [{ name: "postgres", state: "running" }],
|
|
95
|
+
});
|
|
96
|
+
expect(action.tiltTriggerNames).toEqual([]);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
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
|
+
|
|
115
|
+
describe("evaluateReadiness", () => {
|
|
116
|
+
test("ready once every required name reports healthy", () => {
|
|
117
|
+
const outcome = evaluateReadiness(["a", "b"], { a: true, b: true });
|
|
118
|
+
expect(outcome.ready).toBe(true);
|
|
119
|
+
expect(outcome.pending).toEqual([]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("not ready while some names are still unhealthy", () => {
|
|
123
|
+
const outcome = evaluateReadiness(["a", "b"], { a: true, b: false });
|
|
124
|
+
expect(outcome.ready).toBe(false);
|
|
125
|
+
expect(outcome.pending).toEqual(["b"]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("missing entries count as not healthy", () => {
|
|
129
|
+
const outcome = evaluateReadiness(["a", "b"], { a: true });
|
|
130
|
+
expect(outcome.ready).toBe(false);
|
|
131
|
+
expect(outcome.pending).toEqual(["b"]);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure decision logic for the wake gateway (see openspec/changes/prioritized-cold-start).
|
|
3
|
+
*
|
|
4
|
+
* The gateway sits behind a Traefik static (file-provider) route for every
|
|
5
|
+
* `sablier: {enable: true, deferStart: true}` resource. This module decides
|
|
6
|
+
* what to *do* given a snapshot of container state; the actual Docker/Tilt
|
|
7
|
+
* I/O and the healthcheck-polling loop live in server.ts, which is a thin
|
|
8
|
+
* wrapper around this so the decision itself stays testable without a live
|
|
9
|
+
* Docker daemon or Tilt instance.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type ContainerState = "absent" | "created" | "running" | "exited";
|
|
13
|
+
|
|
14
|
+
export interface DependencySnapshot {
|
|
15
|
+
name: string;
|
|
16
|
+
state: ContainerState;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface WakeRequestSnapshot {
|
|
20
|
+
resourceContainerName: string;
|
|
21
|
+
resourceState: ContainerState;
|
|
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;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface WakeAction {
|
|
31
|
+
/** Resources to `tilt trigger` (task 1.5: Tilt never cascades this on its own). */
|
|
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;
|
|
43
|
+
/**
|
|
44
|
+
* Whether to also try Sablier's own Docker-level start for the resource.
|
|
45
|
+
* Only meaningful when the resource's container already exists (D2/D3):
|
|
46
|
+
* Sablier has nothing to manage for a container that was never created.
|
|
47
|
+
*/
|
|
48
|
+
useSablierStart: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Decide which resources need `tilt trigger` and whether Sablier's Docker-level
|
|
53
|
+
* start also applies, given one point-in-time snapshot (D2/D3/D5).
|
|
54
|
+
*/
|
|
55
|
+
export function decideWakeAction(snapshot: WakeRequestSnapshot): WakeAction {
|
|
56
|
+
const tiltTriggerNames: string[] = [];
|
|
57
|
+
const useComposeStart = snapshot.resourceState === "absent" && snapshot.hasComposeInvocation === true;
|
|
58
|
+
|
|
59
|
+
if (snapshot.resourceState !== "running" && !useComposeStart) {
|
|
60
|
+
tiltTriggerNames.push(snapshot.resourceContainerName);
|
|
61
|
+
}
|
|
62
|
+
for (const dep of snapshot.dependencies) {
|
|
63
|
+
if (dep.state !== "running") {
|
|
64
|
+
tiltTriggerNames.push(dep.name);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const useSablierStart = snapshot.resourceState === "exited" || snapshot.resourceState === "created";
|
|
69
|
+
|
|
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;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface WaitOutcome {
|
|
88
|
+
ready: boolean;
|
|
89
|
+
/** Names still not running/healthy when the wait ended (empty when ready). */
|
|
90
|
+
pending: string[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Decide whether a set of health polls (resource + dependencies) means the
|
|
95
|
+
* request can be let through yet, or should keep waiting / time out.
|
|
96
|
+
* `healthy` maps each name from a WakeAction's tiltTriggerNames (plus the
|
|
97
|
+
* resource itself) to its latest observed health state.
|
|
98
|
+
*/
|
|
99
|
+
export function evaluateReadiness(
|
|
100
|
+
requiredNames: string[],
|
|
101
|
+
healthy: Record<string, boolean>,
|
|
102
|
+
): WaitOutcome {
|
|
103
|
+
const pending = requiredNames.filter((name) => !healthy[name]);
|
|
104
|
+
return { ready: pending.length === 0, pending };
|
|
105
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wake gateway: fronts every `sablier.deferStart` resource's Traefik static
|
|
3
|
+
* route (see openspec/changes/prioritized-cold-start). On the first request
|
|
4
|
+
* for a resource with no running container, it triggers the resource (and
|
|
5
|
+
* its cold dependencies) via `tilt trigger`, and/or Sablier's own start for
|
|
6
|
+
* an already-created container, then holds the request until the resource
|
|
7
|
+
* answers its healthcheck or the configured timeout elapses.
|
|
8
|
+
*
|
|
9
|
+
* Config for each request comes from headers a Traefik `headers` middleware
|
|
10
|
+
* injects (set by the file-provider entry generated in
|
|
11
|
+
* traefik_static_routes.star) rather than the path, so the original request
|
|
12
|
+
* path/query reach the real backend unchanged once it's ready:
|
|
13
|
+
* X-Wake-Resource Tilt/Compose resource name (for `tilt trigger` and
|
|
14
|
+
* `docker compose up <service>`).
|
|
15
|
+
* X-Wake-Container The actual Docker container name (for `docker
|
|
16
|
+
* inspect` state/health and the proxy target -- NOT the same string as
|
|
17
|
+
* the resource name; Compose derives it as
|
|
18
|
+
* <project>-<service>-<replica>, confirmed empirically
|
|
19
|
+
* against a live TDK container, e.g.
|
|
20
|
+
* "analytics-autogenerated-data-governance-api-1"
|
|
21
|
+
* for the "data-governance-api" resource).
|
|
22
|
+
* X-Wake-Deps Comma-separated "resourceName=containerName" pairs
|
|
23
|
+
* for required dependencies (task 1.5: Tilt never
|
|
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).
|
|
34
|
+
* X-Wake-Timeout-Ms, X-Wake-Proxy-Port, X-Wake-Session-Duration
|
|
35
|
+
*/
|
|
36
|
+
import {
|
|
37
|
+
decideWakeAction,
|
|
38
|
+
evaluateReadiness,
|
|
39
|
+
wakeTimeoutMs,
|
|
40
|
+
type ContainerState,
|
|
41
|
+
} from "./decide";
|
|
42
|
+
|
|
43
|
+
const TILT_HOST = process.env.TILT_HOST || "host.docker.internal";
|
|
44
|
+
const TILT_PORT = process.env.TILT_PORT || "10350";
|
|
45
|
+
const SABLIER_URL = process.env.SABLIER_URL || "http://sablier:10000";
|
|
46
|
+
const POLL_INTERVAL_MS = 1000;
|
|
47
|
+
|
|
48
|
+
async function run(cmd: string[]): Promise<{ code: number; stdout: string; stderr: string }> {
|
|
49
|
+
const proc = Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
|
|
50
|
+
const [stdout, stderr, code] = await Promise.all([
|
|
51
|
+
new Response(proc.stdout).text(),
|
|
52
|
+
new Response(proc.stderr).text(),
|
|
53
|
+
proc.exited,
|
|
54
|
+
]);
|
|
55
|
+
return { code, stdout, stderr };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function containerState(name: string): Promise<ContainerState> {
|
|
59
|
+
const { code, stdout } = await run([
|
|
60
|
+
"docker", "inspect", "--format", "{{.State.Status}}", name,
|
|
61
|
+
]);
|
|
62
|
+
if (code !== 0) return "absent";
|
|
63
|
+
const status = stdout.trim();
|
|
64
|
+
if (status === "running") return "running";
|
|
65
|
+
if (status === "created") return "created";
|
|
66
|
+
return "exited";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function isHealthy(name: string): Promise<boolean> {
|
|
70
|
+
const { code, stdout } = await run([
|
|
71
|
+
"docker", "inspect",
|
|
72
|
+
"--format", "{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}",
|
|
73
|
+
name,
|
|
74
|
+
]);
|
|
75
|
+
if (code !== 0) return false;
|
|
76
|
+
const status = stdout.trim();
|
|
77
|
+
return status === "healthy" || status === "running";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function tiltTrigger(name: string): Promise<void> {
|
|
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;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Sablier's real blocking-strategy endpoint (GET /api/strategies/blocking,
|
|
131
|
+
* per https://sablierapp.dev/openapi.json): starts the named instance and
|
|
132
|
+
* holds until it's ready or `timeoutMs` elapses, returning that outcome via
|
|
133
|
+
* the X-Sablier-Session-Status header. Only valid for an instance Sablier
|
|
134
|
+
* already knows about (a container that exists with sablier.enable=true) --
|
|
135
|
+
* see decide.ts's useSablierStart.
|
|
136
|
+
*/
|
|
137
|
+
async function sablierBlockingWait(resource: string, sessionDuration: string, timeoutMs: number): Promise<boolean> {
|
|
138
|
+
const params = new URLSearchParams({
|
|
139
|
+
names: resource,
|
|
140
|
+
session_duration: sessionDuration,
|
|
141
|
+
timeout: `${Math.max(1, Math.round(timeoutMs / 1000))}s`,
|
|
142
|
+
});
|
|
143
|
+
try {
|
|
144
|
+
const res = await fetch(`${SABLIER_URL}/api/strategies/blocking?${params}`);
|
|
145
|
+
return res.headers.get("X-Sablier-Session-Status") === "ready";
|
|
146
|
+
} catch {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
interface NamedDep {
|
|
152
|
+
resourceName: string;
|
|
153
|
+
containerName: string;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function parseDeps(raw: string): NamedDep[] {
|
|
157
|
+
return raw
|
|
158
|
+
.split(",")
|
|
159
|
+
.filter(Boolean)
|
|
160
|
+
.map((pair) => {
|
|
161
|
+
const [resourceName, containerName] = pair.split("=");
|
|
162
|
+
return { resourceName, containerName: containerName || resourceName };
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function parseRequest(req: Request) {
|
|
167
|
+
const resource = req.headers.get("X-Wake-Resource") || "";
|
|
168
|
+
const container = req.headers.get("X-Wake-Container") || resource;
|
|
169
|
+
const deps = parseDeps(req.headers.get("X-Wake-Deps") || "");
|
|
170
|
+
const timeoutOverrideMs = Number(req.headers.get("X-Wake-Timeout-Ms")) || undefined;
|
|
171
|
+
const proxyPort = req.headers.get("X-Wake-Proxy-Port") || "80";
|
|
172
|
+
const sessionDuration = req.headers.get("X-Wake-Session-Duration") || "10m";
|
|
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 };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function handleWake(req: Request): Promise<Response> {
|
|
185
|
+
const { resource, container, deps, timeoutOverrideMs, proxyPort, sessionDuration, compose, hasCompose } =
|
|
186
|
+
parseRequest(req);
|
|
187
|
+
if (!resource) {
|
|
188
|
+
return new Response("wake gateway: missing X-Wake-Resource header (set by the generated Traefik static route)", { status: 400 });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const resourceState = await containerState(container);
|
|
192
|
+
const dependencies = await Promise.all(
|
|
193
|
+
deps.map(async (dep) => ({ name: dep.resourceName, state: await containerState(dep.containerName) })),
|
|
194
|
+
);
|
|
195
|
+
|
|
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);
|
|
216
|
+
const deadline = Date.now() + timeoutMs;
|
|
217
|
+
|
|
218
|
+
// Sablier's blocking call, when applicable, both starts AND waits for the
|
|
219
|
+
// resource itself in one round trip.
|
|
220
|
+
const [, resourceReadyViaSablier] = await Promise.all([
|
|
221
|
+
Promise.all(tiltNames.map(tiltTrigger)),
|
|
222
|
+
action.useSablierStart
|
|
223
|
+
? sablierBlockingWait(resource, sessionDuration, timeoutMs)
|
|
224
|
+
: Promise.resolve(false),
|
|
225
|
+
]);
|
|
226
|
+
|
|
227
|
+
const requiredNames = [resource, ...deps.map((d) => d.resourceName)];
|
|
228
|
+
const containerByName: Record<string, string> = { [resource]: container };
|
|
229
|
+
for (const dep of deps) containerByName[dep.resourceName] = dep.containerName;
|
|
230
|
+
|
|
231
|
+
while (Date.now() < deadline) {
|
|
232
|
+
const healthy: Record<string, boolean> = {};
|
|
233
|
+
// Sablier's blocking call already confirmed the resource itself; re-check
|
|
234
|
+
// it too (cheap) so a false result from Sablier still gets a fair retry.
|
|
235
|
+
healthy[resource] = resourceReadyViaSablier || (await isHealthy(container));
|
|
236
|
+
for (const dep of deps) {
|
|
237
|
+
healthy[dep.resourceName] = await isHealthy(dep.containerName);
|
|
238
|
+
}
|
|
239
|
+
const outcome = evaluateReadiness(requiredNames, healthy);
|
|
240
|
+
if (outcome.ready) {
|
|
241
|
+
return proxyThrough(req, container, proxyPort);
|
|
242
|
+
}
|
|
243
|
+
await Bun.sleep(POLL_INTERVAL_MS);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return new Response(
|
|
247
|
+
`wake gateway: timed out after ${timeoutMs}ms waiting for ${resource} and its dependencies to become healthy`,
|
|
248
|
+
{ status: 504 },
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Headers that must not be forwarded verbatim to the upstream fetch: Host
|
|
253
|
+
// would otherwise carry the gateway's own address, and the X-Wake-* control
|
|
254
|
+
// headers are gateway-internal, not part of the real app request.
|
|
255
|
+
const STRIPPED_REQUEST_HEADERS = new Set([
|
|
256
|
+
"host",
|
|
257
|
+
"x-wake-resource",
|
|
258
|
+
"x-wake-container",
|
|
259
|
+
"x-wake-deps",
|
|
260
|
+
"x-wake-timeout-ms",
|
|
261
|
+
"x-wake-proxy-port",
|
|
262
|
+
"x-wake-session-duration",
|
|
263
|
+
"x-wake-compose-project",
|
|
264
|
+
"x-wake-compose-files",
|
|
265
|
+
"x-wake-env-file",
|
|
266
|
+
"x-wake-image",
|
|
267
|
+
]);
|
|
268
|
+
|
|
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> {
|
|
273
|
+
// The original path/query reach the gateway untouched (config travels via
|
|
274
|
+
// X-Wake-* headers, not the path), so they're forwarded as-is.
|
|
275
|
+
const target = new URL(req.url);
|
|
276
|
+
target.protocol = "http:";
|
|
277
|
+
// Set hostname/port separately, not via `.host` -- confirmed empirically
|
|
278
|
+
// that `target.host = "name:port"` silently keeps the URL's original port
|
|
279
|
+
// when one is already present instead of replacing it.
|
|
280
|
+
target.hostname = container;
|
|
281
|
+
target.port = port;
|
|
282
|
+
|
|
283
|
+
const headers = new Headers();
|
|
284
|
+
for (const [key, value] of req.headers) {
|
|
285
|
+
if (!STRIPPED_REQUEST_HEADERS.has(key.toLowerCase())) headers.set(key, value);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const upstream = await fetch(target.toString(), {
|
|
289
|
+
method: req.method,
|
|
290
|
+
headers,
|
|
291
|
+
body: req.method === "GET" || req.method === "HEAD" ? undefined : await req.arrayBuffer(),
|
|
292
|
+
});
|
|
293
|
+
return new Response(upstream.body, { status: upstream.status, headers: upstream.headers });
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
Bun.serve({
|
|
297
|
+
port: Number(process.env.PORT) || 9999,
|
|
298
|
+
// Bun's default idleTimeout (10s) is shorter than a real wake can take
|
|
299
|
+
// (tilt trigger + healthcheck polling) -- confirmed empirically: a request
|
|
300
|
+
// was killed mid-wait with "request timed out after 10 seconds" before this
|
|
301
|
+
// was raised. 255s is Bun's own max for this setting.
|
|
302
|
+
idleTimeout: 255,
|
|
303
|
+
fetch(req) {
|
|
304
|
+
return handleWake(req).catch((err) => {
|
|
305
|
+
console.error("wake gateway error:", err);
|
|
306
|
+
return new Response(`wake gateway error: ${String(err)}`, { status: 500 });
|
|
307
|
+
});
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
console.log(`wake gateway listening on :${Number(process.env.PORT) || 9999}`);
|
|
@@ -2,16 +2,8 @@
|
|
|
2
2
|
# 🏗️ TILT SDK - GOLDEN IMAGE DOCKERFILE GENERATOR
|
|
3
3
|
# =============================================================================
|
|
4
4
|
|
|
5
|
-
# === INLINED CONSTANTS for pure extension loading ===
|
|
6
|
-
get_docker_healthcheck_config = lambda: {"path": "/health", "interval_seconds": 10, "timeout_seconds": 5, "start_period_seconds": 30, "retries": 3, "frontend_start_period_seconds": 30}
|
|
7
|
-
# === END INLINED CONSTANTS ===
|
|
8
|
-
|
|
9
|
-
|
|
10
5
|
load('../../../tilt/common/utils.star', 'Utils')
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
# Load centralized healthcheck configuration
|
|
14
|
-
_docker_health = get_docker_healthcheck_config()
|
|
6
|
+
load('../config/healthcheck.star', 'DOCKER_HEALTHCHECK', 'dockerfile_healthcheck_flags')
|
|
15
7
|
|
|
16
8
|
|
|
17
9
|
# Load project name for dynamic naming
|
|
@@ -76,7 +68,6 @@ RUN apk add --no-cache \
|
|
|
76
68
|
ca-certificates \
|
|
77
69
|
tzdata \
|
|
78
70
|
curl \
|
|
79
|
-
file \
|
|
80
71
|
bash \
|
|
81
72
|
openssl
|
|
82
73
|
|
|
@@ -180,9 +171,11 @@ LABEL layer="l4-backend-bun" \
|
|
|
180
171
|
maintainer="{prefix}"
|
|
181
172
|
|
|
182
173
|
ENV NODE_ENV=production
|
|
174
|
+
# Smaller heap growth and more frequent GC: idle services dominate a 100-service landscape.
|
|
175
|
+
ENV BUN_OPTIONS=--smol
|
|
183
176
|
|
|
184
|
-
#
|
|
185
|
-
|
|
177
|
+
# Service-specific tooling (e.g. hugo) is installed per service by the L4 runtime
|
|
178
|
+
# layer when the manifest lists it in featuresEnabled - not here for every backend.
|
|
186
179
|
|
|
187
180
|
# ⚠️ Infisical CLI is NOT installed in golden L4 images
|
|
188
181
|
# Services use TypeScript SDK (fetch API) or read from env vars - no CLI needed
|
|
@@ -192,7 +185,7 @@ RUN apk add --no-cache hugo
|
|
|
192
185
|
USER bun
|
|
193
186
|
|
|
194
187
|
# Backend-specific healthcheck (checks Bun runtime)
|
|
195
|
-
HEALTHCHECK
|
|
188
|
+
HEALTHCHECK {hc_flags} \
|
|
196
189
|
CMD bun --version || exit 1
|
|
197
190
|
|
|
198
191
|
WORKDIR /app
|
|
@@ -221,7 +214,7 @@ RUN addgroup -S app && adduser -S app -G app
|
|
|
221
214
|
USER app
|
|
222
215
|
|
|
223
216
|
# Backend-specific healthcheck (checks Node.js runtime)
|
|
224
|
-
HEALTHCHECK
|
|
217
|
+
HEALTHCHECK {hc_flags} \
|
|
225
218
|
CMD node --version || exit 1
|
|
226
219
|
|
|
227
220
|
WORKDIR /app
|
|
@@ -246,7 +239,7 @@ ENV NODE_ENV=production
|
|
|
246
239
|
RUN apk add --no-cache curl
|
|
247
240
|
|
|
248
241
|
# Frontend-specific healthcheck (checks Nginx)
|
|
249
|
-
HEALTHCHECK
|
|
242
|
+
HEALTHCHECK {hc_flags_frontend} \
|
|
250
243
|
CMD curl -f http://localhost/ || exit 1
|
|
251
244
|
|
|
252
245
|
WORKDIR /usr/share/nginx/html
|
|
@@ -276,16 +269,13 @@ RUN mkdir -p /cache/bun && chown -R bun:bun /cache/bun
|
|
|
276
269
|
|
|
277
270
|
USER bun
|
|
278
271
|
|
|
279
|
-
#
|
|
280
|
-
|
|
281
|
-
|
|
272
|
+
# Migrators are one-shot jobs (migrate.sh runs to completion), so no healthcheck.
|
|
273
|
+
# The per-service l4_migrator_runtime stage sets HEALTHCHECK NONE as well.
|
|
274
|
+
HEALTHCHECK NONE
|
|
282
275
|
|
|
283
276
|
WORKDIR /app
|
|
284
277
|
""".format(
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
start_period=_docker_health["start_period_seconds"],
|
|
288
|
-
retries=_docker_health["retries"],
|
|
289
|
-
frontend_start_period=_docker_health["frontend_start_period_seconds"],
|
|
278
|
+
hc_flags=dockerfile_healthcheck_flags(),
|
|
279
|
+
hc_flags_frontend=dockerfile_healthcheck_flags(DOCKER_HEALTHCHECK["frontend_start_period_seconds"]),
|
|
290
280
|
prefix=_GOLDEN_PREFIX,
|
|
291
281
|
)
|