@tdk-landscape/tdk-cli-core 1.3.145 → 1.3.147
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 +1 -0
- package/README.md +4 -2
- package/Tiltfile +2 -0
- package/cli/dist/commands/doctor.js +49 -2
- package/cli/dist/utils/doctor-runtime.js +60 -0
- package/cli/dist/utils/host-port-config.js +11 -2
- package/cli/dist/utils/host-port-plan.js +25 -3
- package/cli/dist/utils/service-urls.js +16 -2
- package/cli/dist/utils/smoke.js +28 -0
- package/cli/dist-bundle/chunk-LNZN426S.js +321 -0
- package/cli/dist-bundle/cli.js +3 -3
- package/cli/dist-bundle/{cold-preflight-F2FM46HK.js → cold-preflight-D7CAIHND.js} +1 -1
- package/cli/package.json +1 -1
- package/cli/templates/Tiltfile.hbs +12 -0
- package/discovery/config.star +1 -1
- package/engine/Tiltfile +2 -0
- package/engine/assets/docker/golden-layers.Dockerfile +8 -25
- package/engine/assets/scripts/cleanup-tilt-images.sh +2 -2
- package/engine/topologies/platform/docker/build/frontend_build_plan.star +65 -0
- package/engine/topologies/platform/docker/build/golden_image_build.star +110 -48
- package/engine/topologies/platform/docker/build/golden_image_constants.star +13 -6
- package/engine/topologies/platform/docker/build/golden_image_dockerfile.star +8 -27
- package/engine/topologies/platform/docker/layers/l3_builder_layers.star +3 -1
- package/engine/topologies/platform/docker/layers/prisma/constants.star +2 -1
- package/engine/topologies/tilt/resources/build_concurrency.star +56 -0
- package/engine/topologies/tilt/resources/infra-loader.star +4 -0
- package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +24 -2
- package/engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star +5 -1
- package/engine/topologies/tilt/resources/watchers.star +7 -4
- package/package.json +1 -1
- package/cli/dist-bundle/chunk-47CDYJ5E.js +0 -316
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,7 @@ Notable changes to the `tdk` CLI. Versions match [npm](https://www.npmjs.com/pac
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
- `tdk up` now publishes Traefik on host port 80 (and 443) when they are free, so routed URLs read `http://app.<project>.localhost/<app>` instead of `...localhost:8080/<app>`. It always wanted 80 but only ever tried 8080-8180. On macOS the free-port check treated the kernel's refusal of a non-root bind below 1024 as "busy", although Docker Desktop publishes those ports itself; that also made `TDK_HTTP_PORT=80` fail with "already in use". A project whose saved plan is on 8080 moves to 80 on its next `tdk up` once 80 is free. On Linux and WSL2 a non-root user cannot check port 80, so the 8080-8180 range is kept there. Printed URLs no longer end in `:80`.
|
|
7
8
|
- TDK MCP tools now accept an absolute `projectPath` so a projectless MCP client can run doctor, inspect services and logs, and manage the intended project without depending on the server launch directory.
|
|
8
9
|
- Shared platform stacks (postgres, traefik, proxy, messaging, infisical, monitoring, cdc, elk) now run in Compose projects named `<project>-<stack>` (for example `tdk-erp-system-database-management`) instead of the directory-named `database-management`, `tdk-out` and so on. Two TDK projects on one Docker daemon no longer share them.
|
|
9
10
|
|
package/README.md
CHANGED
|
@@ -52,10 +52,10 @@ mkdir shop && cd shop
|
|
|
52
52
|
tdk project --yes
|
|
53
53
|
tdk resource orders-api --type backend --stack shop --yes
|
|
54
54
|
tdk up shop
|
|
55
|
-
curl http://api.shop.localhost
|
|
55
|
+
curl http://api.shop.localhost/api/orders/health
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
`tdk up` prints the host ports it chose.
|
|
58
|
+
`tdk up` prints the host ports it chose. It uses port 80 when it is free, so URLs carry no port. Otherwise it picks one from 8080-8180, and the URLs it prints include that port (on Linux and WSL2 a non-root user cannot check port 80, so the range is used there). Use the URL `tdk up` prints, or set `TDK_HTTP_PORT`. A resource named `orders-api` is served at `/api/orders` (a trailing `-api` is dropped); `tdk up` prints the exact URLs.
|
|
59
59
|
|
|
60
60
|

|
|
61
61
|
|
|
@@ -141,6 +141,8 @@ Set `TDK_DEBUG=1` before running `tdk up --verbose` to include Starlark debug lo
|
|
|
141
141
|
- [Show HN draft](docs/drafts/show-hn.md)
|
|
142
142
|
- [Architecture and repository map](docs/project-overview.md)
|
|
143
143
|
- [Scale fixture measurements and caveats](docs/benchmarks/scale-bench.md)
|
|
144
|
+
- [Benchmark report: warm and cold start charts, history](https://tdk-landscape.github.io/tdk-cli-core/) (updated on every change to `main` that affects start-up)
|
|
145
|
+
- [How to run the start-time benchmark](scripts/benchmark/README.md#tdk-up-warm-start-benchmark)
|
|
144
146
|
|
|
145
147
|
## History
|
|
146
148
|
|
package/Tiltfile
CHANGED
|
@@ -36,6 +36,7 @@ load('./engine/topologies/tilt/resources/declaration.star', _Orchestrator='Orche
|
|
|
36
36
|
load('./engine/topologies/tilt/resources/ordering.star', _Infra='Infra')
|
|
37
37
|
load('./engine/topologies/tilt/resources/triggers.star', _Watchers='Watchers')
|
|
38
38
|
load('./engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star', _resource_defers_start='resource_defers_start')
|
|
39
|
+
load('./engine/topologies/platform/docker/build/frontend_build_plan.star', _frontend_build_plan='frontend_build_plan')
|
|
39
40
|
|
|
40
41
|
# =============================================================================
|
|
41
42
|
# SPECS (Tech stack and standards)
|
|
@@ -132,6 +133,7 @@ Watchers = _Watchers
|
|
|
132
133
|
# True when a manifest's deferStart is actually active (licensed); decides whether a `<name>-image` prebuild resource exists.
|
|
133
134
|
# The generated Tiltfile loads this from ext://tdk-cli, which resolves to this root Tiltfile.
|
|
134
135
|
resource_defers_start = _resource_defers_start
|
|
136
|
+
FrontendBuildPlan = _frontend_build_plan
|
|
135
137
|
|
|
136
138
|
# Specs
|
|
137
139
|
assert_tech_stack = _assert_tech_stack
|
|
@@ -12,7 +12,7 @@ import { AUTOGENERATED_DIR, CONTAINER_RUNTIME_CHECK, MASTER_CONFIG_FILES, PACKAG
|
|
|
12
12
|
import { isPathDiscovered, readDiscoveryPaths } from "../utils/discovery-paths.js";
|
|
13
13
|
import { checkDiskSpace, dockerDesktopStuck } from "../utils/doctor-host.js";
|
|
14
14
|
import { collectDoctorChecks, createDoctorReport, getDoctorExitCode, } from "../utils/doctor-report.js";
|
|
15
|
-
import { checkHealthRoutes, checkIngressPorts, checkPrivateNpmRegistry, checkTiltResourceHealth, projectConfigEnablesVerdaccio, summarizeServiceProbes, } from "../utils/doctor-runtime.js";
|
|
15
|
+
import { checkHealthRoutes, checkIngressPorts, checkPrivateNpmRegistry, checkTiltResourceHealth, projectConfigEnablesVerdaccio, summarizeAdvertisedProbes, summarizeServiceProbes, summarizeSmokeResults, } from "../utils/doctor-runtime.js";
|
|
16
16
|
import { checkStarlarkLoadExports } from "../utils/doctor-starlark.js";
|
|
17
17
|
import { checkDockerNetworkCapacity, checkDuplicateResourceNames, checkDuplicateResourcePorts, checkFrontendBackendUrls, checkMigrationsInApi, checkNatsBroker, checkPrismaConsistency, checkResourcePackageJson, checkServiceUrlPorts, checkSharedPlatformPostgres, checkSharedStackRoutes, checkTiltInstances, } from "../utils/doctor-wiring.js";
|
|
18
18
|
import { validateEnvFile } from "../utils/env-validator.js";
|
|
@@ -23,8 +23,9 @@ import { createHostPortPlan } from "../utils/host-port-plan.js";
|
|
|
23
23
|
import { findProjectRoot, getPackageVersion } from "../utils/paths.js";
|
|
24
24
|
import { EnvUnreadableError, envSecretValues, redactSecrets, redactValue, } from "../utils/secret-redaction.js";
|
|
25
25
|
import { checkCircularDependencies, checkDependsOnShape, checkSchemaVersions, checkServicePorts, } from "../utils/service-config-checks.js";
|
|
26
|
-
import { buildHealthTargets, pingHealthTargets } from "../utils/service-urls.js";
|
|
26
|
+
import { buildAdvertisedTargets, buildHealthTargets, pingHealthTargets, } from "../utils/service-urls.js";
|
|
27
27
|
import { discoverResourcesFromRoot } from "../utils/services.js";
|
|
28
|
+
import { buildSmokePlans, readOnlySmokePlans, runSmokePlans } from "../utils/smoke.js";
|
|
28
29
|
import { evaluateTdkVersionFloor } from "../utils/tdk-version.js";
|
|
29
30
|
import { findOnPath } from "../utils/which.js";
|
|
30
31
|
export { checkIngressPorts, checkPrivateNpmRegistry, checkTiltResourceHealth, summarizeServiceProbes, summarizeTiltBuildError, } from "../utils/doctor-runtime.js";
|
|
@@ -925,6 +926,50 @@ async function checkServiceHealth(timeoutMs, ingressPort) {
|
|
|
925
926
|
}
|
|
926
927
|
return summarizeServiceProbes(await pingHealthTargets(targets, timeoutMs));
|
|
927
928
|
}
|
|
929
|
+
/**
|
|
930
|
+
* Runs the GET steps of each service's "smoke" block through its public URL. These are the checks `tdk up` already runs, minus the
|
|
931
|
+
* writes, so a service with an empty /health but a missing list route is caught before anyone clicks a dead link.
|
|
932
|
+
*
|
|
933
|
+
* Like the health ping, this only runs once services answer. A stopped stack is skipped, not failed.
|
|
934
|
+
*/
|
|
935
|
+
async function checkServiceSmoke(timeoutMs, ingressPort) {
|
|
936
|
+
const projectRoot = findProjectRoot() ?? process.cwd();
|
|
937
|
+
const resources = discoverResourcesFromRoot(projectRoot);
|
|
938
|
+
const plans = readOnlySmokePlans(buildSmokePlans(resources, ingressPort), {
|
|
939
|
+
timeoutSeconds: Math.ceil(timeoutMs / 1000),
|
|
940
|
+
});
|
|
941
|
+
if (plans.length === 0) {
|
|
942
|
+
return summarizeSmokeResults([]);
|
|
943
|
+
}
|
|
944
|
+
const probes = await pingHealthTargets(buildHealthTargets(resources, ingressPort), timeoutMs);
|
|
945
|
+
if (!probes.some((probe) => probe.status !== undefined)) {
|
|
946
|
+
return {
|
|
947
|
+
name: "Service Smoke",
|
|
948
|
+
didPass: true,
|
|
949
|
+
isSkipped: true,
|
|
950
|
+
message: "No services responded - skipped smoke checks",
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
return summarizeSmokeResults(await runSmokePlans(plans));
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Requests the URL each routable service advertises, the same one `tdk networks` and the TDK App show, and warns on a 404.
|
|
957
|
+
* Like the other runtime checks, it only runs once services answer. A stopped stack is skipped, not reported.
|
|
958
|
+
*/
|
|
959
|
+
async function checkAdvertisedEndpoints(timeoutMs, ingressPort) {
|
|
960
|
+
const projectRoot = findProjectRoot() ?? process.cwd();
|
|
961
|
+
const resources = discoverResourcesFromRoot(projectRoot);
|
|
962
|
+
const targets = buildAdvertisedTargets(resources, ingressPort);
|
|
963
|
+
if (targets.length === 0) {
|
|
964
|
+
return {
|
|
965
|
+
name: "Advertised Endpoints",
|
|
966
|
+
didPass: true,
|
|
967
|
+
isSkipped: true,
|
|
968
|
+
message: "No routable services to check",
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
return summarizeAdvertisedProbes(await pingHealthTargets(targets, timeoutMs));
|
|
972
|
+
}
|
|
928
973
|
export const doctorCommand = new Command("doctor")
|
|
929
974
|
.description("Check environment readiness for TDK")
|
|
930
975
|
.option("--json", "Output a versioned JSON readiness report", false)
|
|
@@ -1087,6 +1132,8 @@ export const doctorCommand = new Command("doctor")
|
|
|
1087
1132
|
// Runs last: needs routable services (and working Traefik) to mean anything.
|
|
1088
1133
|
if (options.ping) {
|
|
1089
1134
|
projectChecks.push(() => checkServiceHealth(pingTimeout, hostPortPlan?.ingressHttp));
|
|
1135
|
+
projectChecks.push(() => checkServiceSmoke(pingTimeout, hostPortPlan?.ingressHttp));
|
|
1136
|
+
projectChecks.push(() => checkAdvertisedEndpoints(pingTimeout, hostPortPlan?.ingressHttp));
|
|
1090
1137
|
}
|
|
1091
1138
|
// Right after installing, people run `tdk doctor` before they have a
|
|
1092
1139
|
// project. Only the machine checks mean anything there; the project checks
|
|
@@ -462,6 +462,66 @@ export function summarizeServiceProbes(probes) {
|
|
|
462
462
|
: "Check container state and routing: docker ps, then tdk networks to compare the advertised URLs against Traefik's routers",
|
|
463
463
|
};
|
|
464
464
|
}
|
|
465
|
+
/**
|
|
466
|
+
* Summarizes the read-only smoke checks. A 404 here usually means the service never registered the route its service.json
|
|
467
|
+
* names, which the health ping cannot see because /health answers regardless.
|
|
468
|
+
*/
|
|
469
|
+
export function summarizeSmokeResults(results) {
|
|
470
|
+
if (results.length === 0) {
|
|
471
|
+
return {
|
|
472
|
+
name: "Service Smoke",
|
|
473
|
+
didPass: true,
|
|
474
|
+
isSkipped: true,
|
|
475
|
+
message: "No read-only smoke checks declared",
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
const failed = results.filter((result) => !result.ok);
|
|
479
|
+
if (failed.length === 0) {
|
|
480
|
+
return {
|
|
481
|
+
name: "Service Smoke",
|
|
482
|
+
didPass: true,
|
|
483
|
+
message: `All ${formatCount(results.length, "service")} pass their smoke checks`,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
const details = failed.map((result) => result.failure ?? `${result.name}: failed`).join("\n ");
|
|
487
|
+
return {
|
|
488
|
+
name: "Service Smoke",
|
|
489
|
+
didPass: false,
|
|
490
|
+
message: `${formatCount(failed.length, "service")} failing a smoke check (${results.length - failed.length}/${results.length} passing):\n ${details}`,
|
|
491
|
+
fix: 'Add the route the smoke step expects, or correct its "path" in the service.json "smoke" block. Then re-run tdk doctor.',
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Flags advertised links that answer 404. Only a 404 counts: a route that answers with any other status is reachable, and many APIs
|
|
496
|
+
* have no root route, so this is a warning and never fails the run on its own.
|
|
497
|
+
*/
|
|
498
|
+
export function summarizeAdvertisedProbes(probes) {
|
|
499
|
+
const answered = probes.filter((probe) => probe.status !== undefined);
|
|
500
|
+
if (answered.length === 0) {
|
|
501
|
+
return {
|
|
502
|
+
name: "Advertised Endpoints",
|
|
503
|
+
didPass: true,
|
|
504
|
+
isSkipped: true,
|
|
505
|
+
message: "No services responded - skipped advertised endpoint check",
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
const dead = answered.filter((probe) => probe.status === 404);
|
|
509
|
+
if (dead.length === 0) {
|
|
510
|
+
return {
|
|
511
|
+
name: "Advertised Endpoints",
|
|
512
|
+
didPass: true,
|
|
513
|
+
message: `${formatCount(answered.length, "advertised endpoint")} reachable`,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
const details = dead.map((probe) => `${probe.name} (404)\n ${probe.url}`).join("\n ");
|
|
517
|
+
return {
|
|
518
|
+
name: "Advertised Endpoints",
|
|
519
|
+
didPass: true,
|
|
520
|
+
isWarning: true,
|
|
521
|
+
message: `${formatCount(dead.length, "advertised link")} returns 404:\n ${details}`,
|
|
522
|
+
fix: 'Nothing handles this path, so the link is dead. Add a route for it, or declare a read-only "smoke" step on a real route in service.json. The URL tdk networks and the TDK App print stays the same until the CLI changes how it reports it.',
|
|
523
|
+
};
|
|
524
|
+
}
|
|
465
525
|
/**
|
|
466
526
|
* Catches host ports taken by something other than this project's containers,
|
|
467
527
|
* most often a local Postgres on 5432 or a web server on 80. Docker-held 80/443
|
|
@@ -4,7 +4,7 @@ import { execFileSync } from "node:child_process";
|
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { PROJECT_JSON } from "./constants.js";
|
|
7
|
-
import { createHostPortPlan, isHostPortAvailable } from "./host-port-plan.js";
|
|
7
|
+
import { createHostPortPlan, isHostPortAvailable, PREFERRED_HOST_PORTS, } from "./host-port-plan.js";
|
|
8
8
|
const CONFIG_PATH = join(".tdk", ".tdk-out", "host-ports.json");
|
|
9
9
|
export function isDockerPortOwnedByProject(dockerPs, projectPrefix, port) {
|
|
10
10
|
return dockerPs.split("\n").some((line) => {
|
|
@@ -78,12 +78,21 @@ export async function getHostPortPlan(projectRoot, options = {}) {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
const ownedByProject = (port) => isDockerPortOwnedByProject(dockerPs, prefix, port);
|
|
81
|
-
const
|
|
81
|
+
const isPortFree = options.isPortFree ?? isHostPortAvailable;
|
|
82
|
+
const isAvailable = async (port) => (await isPortFree(port)) || ownedByProject(port);
|
|
82
83
|
if (saved && !hasOverride) {
|
|
83
84
|
for (const port of [saved.ingressHttp, saved.ingressHttps, saved.postgres]) {
|
|
84
85
|
if (!(await isAvailable(port)))
|
|
85
86
|
return createHostPortPlan({ isAvailable });
|
|
86
87
|
}
|
|
88
|
+
// A plan saved on a fallback port (8080) moves back to the preferred one (80)
|
|
89
|
+
// once that is free, so URLs lose the port without deleting host-ports.json.
|
|
90
|
+
for (const [key, preferred] of Object.entries(PREFERRED_HOST_PORTS)) {
|
|
91
|
+
const savedPort = saved[key];
|
|
92
|
+
if (preferred !== undefined && savedPort !== preferred && (await isAvailable(preferred))) {
|
|
93
|
+
return createHostPortPlan({ isAvailable });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
87
96
|
return saved;
|
|
88
97
|
}
|
|
89
98
|
return createHostPortPlan({ isAvailable });
|
|
@@ -13,12 +13,28 @@ const ENV_KEYS = {
|
|
|
13
13
|
postgres: "TDK_POSTGRES_PORT",
|
|
14
14
|
};
|
|
15
15
|
const REQUESTED_PORTS = { ingressHttp: 80, ingressHttps: 443, postgres: 5432 };
|
|
16
|
+
// Tried before the fallback range so routed URLs need no port. Postgres keeps
|
|
17
|
+
// its range: a local Postgres on 5432 is common, and no URL carries that port.
|
|
18
|
+
export const PREFERRED_HOST_PORTS = {
|
|
19
|
+
ingressHttp: REQUESTED_PORTS.ingressHttp,
|
|
20
|
+
ingressHttps: REQUESTED_PORTS.ingressHttps,
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Whether a failed bind on 127.0.0.1 means the port is still free for Docker. Only macOS
|
|
24
|
+
* refuses a non-root bind below 1024 while Docker Desktop publishes it through a privileged
|
|
25
|
+
* helper. Callers check for a listener first, so a bind error here is never another process.
|
|
26
|
+
*/
|
|
27
|
+
export function isPrivilegedBindFree(errorCode, platform) {
|
|
28
|
+
return errorCode === "EACCES" && platform === "darwin";
|
|
29
|
+
}
|
|
16
30
|
export async function isHostPortAvailable(port) {
|
|
17
31
|
if (await hasLocalListener(port))
|
|
18
32
|
return false;
|
|
19
33
|
return new Promise((resolve) => {
|
|
20
34
|
const server = createServer();
|
|
21
|
-
server.once("error", () =>
|
|
35
|
+
server.once("error", (error) => {
|
|
36
|
+
resolve(isPrivilegedBindFree(error.code, process.platform));
|
|
37
|
+
});
|
|
22
38
|
// Docker Desktop and Windows publish these host ports through IPv4 loopback.
|
|
23
39
|
// Match that target instead of probing wildcard binds that may differ by OS.
|
|
24
40
|
server.listen(port, "127.0.0.1", () => server.close(() => resolve(true)));
|
|
@@ -42,9 +58,13 @@ export async function createHostPortPlan(options = {}) {
|
|
|
42
58
|
const envValue = env[ENV_KEYS[key]];
|
|
43
59
|
const explicit = envValue !== undefined && envValue.trim() !== "";
|
|
44
60
|
const range = options.ranges?.[key] ?? DEFAULT_HOST_PORT_RANGES[key];
|
|
61
|
+
const preferred = PREFERRED_HOST_PORTS[key];
|
|
45
62
|
const candidates = explicit
|
|
46
63
|
? [parsePort(ENV_KEYS[key], envValue)]
|
|
47
|
-
:
|
|
64
|
+
: [
|
|
65
|
+
...(preferred === undefined ? [] : [preferred]),
|
|
66
|
+
...Array.from({ length: range.end - range.start + 1 }, (_, index) => range.start + index),
|
|
67
|
+
];
|
|
48
68
|
let chosen;
|
|
49
69
|
for (const port of candidates) {
|
|
50
70
|
if (reserved.has(port))
|
|
@@ -65,7 +85,9 @@ export async function createHostPortPlan(options = {}) {
|
|
|
65
85
|
explicitFlags[key] = explicit;
|
|
66
86
|
reasons[key] = explicit
|
|
67
87
|
? "explicit override"
|
|
68
|
-
:
|
|
88
|
+
: chosen === preferred
|
|
89
|
+
? "requested port is free"
|
|
90
|
+
: `selected from fallback range ${range.start}-${range.end}`;
|
|
69
91
|
}
|
|
70
92
|
return {
|
|
71
93
|
...selected,
|
|
@@ -26,7 +26,11 @@ export function resolveSubdomainBases(ingressPort) {
|
|
|
26
26
|
try {
|
|
27
27
|
const u = new URL(raw.includes("://") ? raw : `http://${raw}`);
|
|
28
28
|
const host = u.hostname;
|
|
29
|
-
|
|
29
|
+
// URL drops a default port (80 for http, 443 for https), so u.port is empty
|
|
30
|
+
// for http://x.localhost:80. Keep it empty instead of re-adding ingressPort.
|
|
31
|
+
const defaultPort = u.protocol === "https:" ? "443" : "80";
|
|
32
|
+
const explicitPort = u.port || (ingressPort ? String(ingressPort) : "");
|
|
33
|
+
const port = explicitPort === defaultPort ? "" : explicitPort;
|
|
30
34
|
if (host === "localhost" || /^\d+\.\d+\.\d+\.\d+$/.test(host)) {
|
|
31
35
|
return {
|
|
32
36
|
appBase: `${u.protocol}//${host}${port ? `:${port}` : ""}`,
|
|
@@ -63,6 +67,16 @@ export function resolveServicePath(resource) {
|
|
|
63
67
|
* skipped rather than reported as unreachable.
|
|
64
68
|
*/
|
|
65
69
|
export function buildHealthTargets(resources, ingressPort) {
|
|
70
|
+
return routableTargets(resources, ingressPort, (path) => appendHealthPath(path));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The URL `tdk up` and `tdk networks` advertise for each routable service, with no suffix. A service can answer /health while
|
|
74
|
+
* this URL is a 404 (nothing handles the bare path), which is the dead link users click.
|
|
75
|
+
*/
|
|
76
|
+
export function buildAdvertisedTargets(resources, ingressPort) {
|
|
77
|
+
return routableTargets(resources, ingressPort, (path) => path);
|
|
78
|
+
}
|
|
79
|
+
function routableTargets(resources, ingressPort, toPath) {
|
|
66
80
|
const { appBase, apiBase } = resolveSubdomainBases(ingressPort);
|
|
67
81
|
return resources
|
|
68
82
|
.filter((resource) => resource.config?.appType === "frontend" || isApiServiceType(resource.config?.appType))
|
|
@@ -71,7 +85,7 @@ export function buildHealthTargets(resources, ingressPort) {
|
|
|
71
85
|
return {
|
|
72
86
|
name: resource.name,
|
|
73
87
|
appType: resource.config.appType,
|
|
74
|
-
url: `${base}${
|
|
88
|
+
url: `${base}${toPath(resolveServicePath(resource))}`,
|
|
75
89
|
};
|
|
76
90
|
});
|
|
77
91
|
}
|
package/cli/dist/utils/smoke.js
CHANGED
|
@@ -105,6 +105,34 @@ export function buildSmokePlans(resources, ingressPort) {
|
|
|
105
105
|
}
|
|
106
106
|
return plans;
|
|
107
107
|
}
|
|
108
|
+
/** Caps each read-only smoke check so a doctor run cannot wait the full `tdk up` budget per service. */
|
|
109
|
+
const DOCTOR_SMOKE_TIMEOUT_SECONDS = 10;
|
|
110
|
+
/**
|
|
111
|
+
* The GET-only part of each smoke block, for `tdk doctor`. Doctor must never write to a service, and a step whose path reads
|
|
112
|
+
* a value saved by an earlier write (`{{name}}`) cannot run on its own, so both are dropped. A service with no such steps is left out.
|
|
113
|
+
* `bodyContains` is also cleared: it asserts on data that earlier writes created, so doctor would fail a healthy route whose data was removed.
|
|
114
|
+
* The status check stays, which is what shows a route exists.
|
|
115
|
+
*/
|
|
116
|
+
export function readOnlySmokePlans(plans, options = {}) {
|
|
117
|
+
const cap = options.timeoutSeconds ?? DOCTOR_SMOKE_TIMEOUT_SECONDS;
|
|
118
|
+
return plans.flatMap((plan) => {
|
|
119
|
+
const steps = plan.smoke.steps
|
|
120
|
+
.filter((step) => (step.method ?? "GET").toUpperCase() === "GET" &&
|
|
121
|
+
step.body === undefined &&
|
|
122
|
+
!step.path.includes("{{"))
|
|
123
|
+
.map((step) => ({ ...step, bodyContains: undefined }));
|
|
124
|
+
if (steps.length === 0)
|
|
125
|
+
return [];
|
|
126
|
+
const budget = Math.min(plan.smoke.timeoutSeconds ?? DEFAULT_SMOKE_TIMEOUT_SECONDS, cap);
|
|
127
|
+
return [
|
|
128
|
+
{
|
|
129
|
+
name: plan.name,
|
|
130
|
+
baseUrl: plan.baseUrl,
|
|
131
|
+
smoke: { via: plan.smoke.via, timeoutSeconds: budget, steps },
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
});
|
|
135
|
+
}
|
|
108
136
|
function readPath(value, path) {
|
|
109
137
|
const parts = path.replace(/^\$\.?/, "").match(/[^.[\]]+/g) ?? [];
|
|
110
138
|
let current = value;
|