@tdk-landscape/tdk-cli-core 1.3.63 → 1.3.64
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/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,9 @@ Notable changes to the `tdk` CLI. Versions match [npm](https://www.npmjs.com/pac
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
- `traefik.healthCheck` in service.json (the path Traefik's load balancer probes, documented in the schema) is now honored. It was read by discovery but never used, so Traefik always probed `healthCheckPath`. It defaults to `healthCheckPath`, so manifests that only set that generate the same output as before.
|
|
8
|
+
- `tdk doctor` checks that every backend defines the health routes it is probed on (`healthCheckPath` for the container, `traefik.healthCheck` for Traefik, both defaulting to `/health`) somewhere in `src/`. A backend without them never turns healthy, so Traefik returns 404, which used to surface only after a slow image build.
|
|
9
|
+
|
|
7
10
|
## 1.3.62 (2026-09-28)
|
|
8
11
|
|
|
9
12
|
- `tdk doctor` no longer skips the service health check when Traefik answers with an error. A 404 (no route yet) or 502/503/504 (route found, container not answering) is reported as a failure that names the service and URL, instead of being read as "Traefik never bound :80". It is skipped only when every probe fails to connect.
|
|
@@ -4,7 +4,7 @@ import { dirname, join, normalize } from "node:path";
|
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
import { MASTER_CONFIG_FILES, QUICKSTART_DOCS_URL, REQUIRED_PACKAGE_SCRIPTS, } from "../utils/constants.js";
|
|
7
|
-
import { checkHostPorts, checkIngressPorts, checkPrivateNpmRegistry, checkTiltResourceHealth, summarizeServiceProbes, } from "../utils/doctor-runtime.js";
|
|
7
|
+
import { checkHealthRoutes, checkHostPorts, checkIngressPorts, checkPrivateNpmRegistry, checkTiltResourceHealth, summarizeServiceProbes, } from "../utils/doctor-runtime.js";
|
|
8
8
|
import { validateEnvFile } from "../utils/env-validator.js";
|
|
9
9
|
import { formatCount } from "../utils/formatting.js";
|
|
10
10
|
import { findProjectRoot } from "../utils/paths.js";
|
|
@@ -683,6 +683,8 @@ export const doctorCommand = new Command("doctor")
|
|
|
683
683
|
checkGeneratedProjectRuntimeAssets,
|
|
684
684
|
checkStarlarkLoadExports,
|
|
685
685
|
checkStartupScripts,
|
|
686
|
+
// A backend without its health route never turns healthy; Traefik 404s it.
|
|
687
|
+
() => checkHealthRoutes(),
|
|
686
688
|
checkTypeScriptTypeDependencies,
|
|
687
689
|
checkFrontendDockerPreflight,
|
|
688
690
|
checkEnvironmentVariables,
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { execSync } from "node:child_process";
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
3
3
|
import { createServer } from "node:net";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { formatCount } from "./formatting.js";
|
|
6
6
|
import { findProjectRoot } from "./paths.js";
|
|
7
7
|
import { getProjectName } from "./service-urls.js";
|
|
8
|
-
import { discoverResources } from "./services.js";
|
|
8
|
+
import { discoverResources, discoverResourcesFromRoot } from "./services.js";
|
|
9
9
|
const EXEC_TIMEOUT_MS = 10_000;
|
|
10
10
|
const REGISTRY_PROBE_TIMEOUT_MS = 3_000;
|
|
11
11
|
const DEFAULT_VERDACCIO_URL = "http://localhost:4873";
|
|
@@ -636,4 +636,109 @@ export function describeTiltFailure(failure, exec = execSync) {
|
|
|
636
636
|
}
|
|
637
637
|
return failure.updateStatus || failure.runtimeStatus || "unknown error";
|
|
638
638
|
}
|
|
639
|
+
const SOURCE_FILE = /\.(ts|tsx|mts|js|mjs|cjs)$/;
|
|
640
|
+
const SKIPPED_DIRS = new Set(["node_modules", "dist", ".autogenerated", "tests", "__tests__"]);
|
|
641
|
+
function listSourceFiles(dir) {
|
|
642
|
+
const files = [];
|
|
643
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
644
|
+
if (entry.isDirectory()) {
|
|
645
|
+
if (!SKIPPED_DIRS.has(entry.name))
|
|
646
|
+
files.push(...listSourceFiles(join(dir, entry.name)));
|
|
647
|
+
}
|
|
648
|
+
else if (SOURCE_FILE.test(entry.name) && !/\.(test|spec)\./.test(entry.name)) {
|
|
649
|
+
files.push(join(dir, entry.name));
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return files;
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* The two paths a backend is probed on, resolved like the engine does:
|
|
656
|
+
* the container healthcheck uses `healthCheckPath` (default /health), and
|
|
657
|
+
* Traefik's load balancer uses `traefik.healthCheck`, defaulting to the
|
|
658
|
+
* container's path.
|
|
659
|
+
*/
|
|
660
|
+
export function resolveHealthPaths(config) {
|
|
661
|
+
const container = config?.healthCheckPath || "/health";
|
|
662
|
+
return { container, traefik: config?.traefik?.healthCheck || container };
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Backends whose source never mentions a path they are probed on as a string
|
|
666
|
+
* literal ('/health', "/health" or `/health`). Covers Hono, Express, Elysia,
|
|
667
|
+
* Fastify and hand-rolled `url.pathname === "/health"` checks. Services
|
|
668
|
+
* without a src/ directory are skipped, since there is nothing to read.
|
|
669
|
+
*/
|
|
670
|
+
export function findMissingHealthRoutes(resources) {
|
|
671
|
+
const missing = [];
|
|
672
|
+
for (const resource of resources) {
|
|
673
|
+
if (resource.config?.appType !== "backend")
|
|
674
|
+
continue;
|
|
675
|
+
const srcDir = join(resource.path, "src");
|
|
676
|
+
if (!existsSync(srcDir))
|
|
677
|
+
continue;
|
|
678
|
+
const files = listSourceFiles(srcDir);
|
|
679
|
+
if (files.length === 0)
|
|
680
|
+
continue;
|
|
681
|
+
const sources = files.map((file) => readFileSync(file, "utf-8"));
|
|
682
|
+
const { container, traefik } = resolveHealthPaths(resource.config);
|
|
683
|
+
const probes = container === traefik
|
|
684
|
+
? [{ path: container, usedBy: "container and Traefik healthchecks" }]
|
|
685
|
+
: [
|
|
686
|
+
{ path: container, usedBy: "container healthcheck" },
|
|
687
|
+
{ path: traefik, usedBy: "Traefik healthcheck" },
|
|
688
|
+
];
|
|
689
|
+
for (const { path, usedBy } of probes) {
|
|
690
|
+
const literals = ["'", '"', "`"].map((q) => `${q}${path}${q}`);
|
|
691
|
+
if (!sources.some((source) => literals.some((literal) => source.includes(literal)))) {
|
|
692
|
+
missing.push({ name: resource.name, healthPath: path, usedBy });
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return missing;
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* A backend without its health route never passes the container healthcheck,
|
|
700
|
+
* so Traefik never routes to it and every request gets a 404 - after a slow
|
|
701
|
+
* image build, with nothing in `tdk up` saying why. Catch it before starting.
|
|
702
|
+
*/
|
|
703
|
+
export function checkHealthRoutes(projectRoot = findProjectRoot() ?? process.cwd()) {
|
|
704
|
+
let resources = [];
|
|
705
|
+
try {
|
|
706
|
+
resources = discoverResourcesFromRoot(projectRoot);
|
|
707
|
+
}
|
|
708
|
+
catch {
|
|
709
|
+
return {
|
|
710
|
+
name: "Health Routes",
|
|
711
|
+
didPass: true,
|
|
712
|
+
isSkipped: true,
|
|
713
|
+
message: "Could not read services - skipped health route check",
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
const backends = resources.filter((r) => r.config?.appType === "backend");
|
|
717
|
+
if (backends.length === 0) {
|
|
718
|
+
return {
|
|
719
|
+
name: "Health Routes",
|
|
720
|
+
didPass: true,
|
|
721
|
+
isSkipped: true,
|
|
722
|
+
message: "No backend services to check",
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
const missing = findMissingHealthRoutes(backends);
|
|
726
|
+
if (missing.length === 0) {
|
|
727
|
+
return {
|
|
728
|
+
name: "Health Routes",
|
|
729
|
+
didPass: true,
|
|
730
|
+
message: `All ${formatCount(backends.length, "backend")} define their health route`,
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
const details = missing
|
|
734
|
+
.map((m) => `${m.name}: no "${m.healthPath}" route in src/ (probed by the ${m.usedBy})`)
|
|
735
|
+
.join("\n ");
|
|
736
|
+
return {
|
|
737
|
+
name: "Health Routes",
|
|
738
|
+
didPass: false,
|
|
739
|
+
message: `Backends without a health route (the container never turns healthy, so Traefik returns 404):\n ${details}`,
|
|
740
|
+
fix: `Add the route, e.g. for Hono: app.get("${missing[0]?.healthPath}", (c) => c.json({ status: "ok" })). ` +
|
|
741
|
+
'To use a different path, set "healthCheckPath" (container healthcheck) or "traefik.healthCheck" (Traefik) in its service.json.',
|
|
742
|
+
};
|
|
743
|
+
}
|
|
639
744
|
//# sourceMappingURL=doctor-runtime.js.map
|
package/cli/package.json
CHANGED
|
@@ -287,7 +287,9 @@ def _apply_manifest_defaults(manifest, resource_path):
|
|
|
287
287
|
computed_traefik = {
|
|
288
288
|
'host': stack + '.backend.' + PlatformDockerConstants.PROJECT_NAME + '.local',
|
|
289
289
|
'pathPrefix': default_path_prefix,
|
|
290
|
-
|
|
290
|
+
# Follows healthCheckPath so a manifest that only sets that keeps one
|
|
291
|
+
# path for both probes; `traefik.healthCheck` overrides just Traefik's.
|
|
292
|
+
'healthCheck': result.get('healthCheckPath') or HEALTH_CHECK_PATH,
|
|
291
293
|
}
|
|
292
294
|
|
|
293
295
|
# Check for overrides
|
|
@@ -248,13 +248,16 @@ def _generate_single_backend_entry(resource_path, resource_name, res, manifest,
|
|
|
248
248
|
traefik_labels = ' - "traefik.enable=false"'
|
|
249
249
|
healthcheck_section = ""
|
|
250
250
|
else:
|
|
251
|
+
# Traefik's load-balancer check can be overridden separately in service.json
|
|
252
|
+
# (`traefik.healthCheck`); it defaults to the container check's path.
|
|
253
|
+
traefik_health_path = traefik_cfg.get('healthCheck') or health_path
|
|
251
254
|
traefik_labels = get_backend_traefik_labels(
|
|
252
255
|
resource_entry_name=resource_entry_name,
|
|
253
256
|
traefik_host=traefik_host,
|
|
254
257
|
traefik_path=traefik_path,
|
|
255
258
|
traefik_resource_name=traefik_resource_name,
|
|
256
259
|
internal_port=internal_port,
|
|
257
|
-
health_path=
|
|
260
|
+
health_path=traefik_health_path,
|
|
258
261
|
manifest=manifest,
|
|
259
262
|
)
|
|
260
263
|
healthcheck_section = """ healthcheck:
|
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.64",
|
|
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": {
|