@blogic-cz/agent-tools 0.14.38 → 0.14.39
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/package.json +1 -1
- package/src/config/loader.ts +1 -0
- package/src/config/types.ts +7 -0
- package/src/db-tool/service.ts +3 -20
- package/src/k8s-tool/service.ts +46 -0
- package/src/shared/k8s-probe.ts +23 -0
package/package.json
CHANGED
package/src/config/loader.ts
CHANGED
|
@@ -80,6 +80,7 @@ const K8sConfigSchema = Schema.Struct({
|
|
|
80
80
|
clusterId: Schema.String,
|
|
81
81
|
namespaces: Schema.Record(Schema.String, Schema.String),
|
|
82
82
|
timeoutMs: Schema.optionalKey(Schema.Number),
|
|
83
|
+
apiProbeTimeoutMs: Schema.optionalKey(Schema.Number),
|
|
83
84
|
prerequisites: Schema.optionalKey(PrerequisitesSchema),
|
|
84
85
|
vpn: Schema.optionalKey(Schema.String),
|
|
85
86
|
});
|
package/src/config/types.ts
CHANGED
|
@@ -71,6 +71,13 @@ export type K8sConfig = ProfilePrerequisites & {
|
|
|
71
71
|
/** Named namespaces, e.g. { test: "my-app-test", prod: "my-app-prod" } */
|
|
72
72
|
namespaces: Record<string, string>;
|
|
73
73
|
timeoutMs?: number;
|
|
74
|
+
/**
|
|
75
|
+
* Timeout in milliseconds for the cheap Kubernetes API-server reachability probe run
|
|
76
|
+
* before each command. When the cluster API is unreachable (VPN down / off the office
|
|
77
|
+
* network, or the API degraded) a real kubectl command hangs until `timeoutMs`; the
|
|
78
|
+
* probe lets it fail fast with a clear message instead. Set to 0 to disable. Defaults to 2000.
|
|
79
|
+
*/
|
|
80
|
+
apiProbeTimeoutMs?: number;
|
|
74
81
|
};
|
|
75
82
|
|
|
76
83
|
/** Single database environment connection details */
|
package/src/db-tool/service.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { isPrerequisiteRunError } from "#shared/prerequisites/errors";
|
|
|
8
8
|
import { resolveEnvTemplate } from "#shared/env-template";
|
|
9
9
|
import { resolveEnvironmentScopedPrerequisites } from "#shared/prerequisites/config";
|
|
10
10
|
import { runWithProfilePrerequisites } from "#shared/prerequisites/runtime";
|
|
11
|
+
import { buildApiProbeArgs } from "#shared/k8s-probe";
|
|
11
12
|
import { DbConfigService, DbConfigServiceLayer, TUNNEL_CHECK_INTERVAL_MS } from "./config-service";
|
|
12
13
|
import {
|
|
13
14
|
DbConnectionError,
|
|
@@ -51,26 +52,8 @@ export function resolveDbAccessMode(
|
|
|
51
52
|
};
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
* Hitting `/version` via `--raw` is the cheapest authenticated round-trip; `--request-timeout`
|
|
57
|
-
* bounds it so an unreachable server (VPN down, off the office network) fails fast instead of
|
|
58
|
-
* letting a subsequent `kubectl port-forward` hang on a silent TCP connect.
|
|
59
|
-
*/
|
|
60
|
-
export function buildApiProbeArgs(
|
|
61
|
-
kubeconfig: string | undefined,
|
|
62
|
-
context: string,
|
|
63
|
-
timeoutMs: number,
|
|
64
|
-
): string[] {
|
|
65
|
-
return [
|
|
66
|
-
...(kubeconfig ? ["--kubeconfig", kubeconfig] : []),
|
|
67
|
-
"--context",
|
|
68
|
-
context,
|
|
69
|
-
"get",
|
|
70
|
-
"--raw=/version",
|
|
71
|
-
`--request-timeout=${timeoutMs}ms`,
|
|
72
|
-
];
|
|
73
|
-
}
|
|
55
|
+
// Re-exported from the shared probe so existing `#db/service` consumers keep working.
|
|
56
|
+
export { buildApiProbeArgs } from "#shared/k8s-probe";
|
|
74
57
|
|
|
75
58
|
export class DbService extends Context.Service<
|
|
76
59
|
DbService,
|
package/src/k8s-tool/service.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { collectProcessOutput, quoteShellArg } from "#shared/exec";
|
|
|
15
15
|
import { resolveEnvTemplate } from "#shared/env-template";
|
|
16
16
|
import { isPrerequisiteRunError } from "#shared/prerequisites/errors";
|
|
17
17
|
import { runWithProfilePrerequisites } from "#shared/prerequisites/runtime";
|
|
18
|
+
import { buildApiProbeArgs } from "#shared/k8s-probe";
|
|
18
19
|
import { isKubectlCommandAllowed } from "./security";
|
|
19
20
|
|
|
20
21
|
export class K8sService extends Context.Service<
|
|
@@ -146,6 +147,40 @@ export class K8sService extends Context.Service<
|
|
|
146
147
|
),
|
|
147
148
|
);
|
|
148
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Cheap pre-flight: is the cluster API reachable right now? Returns true when the probe
|
|
152
|
+
* is disabled (apiProbeTimeoutMs <= 0) so behaviour is unchanged unless a probe can run.
|
|
153
|
+
* A false result lets the command fail fast instead of hanging until the full timeoutMs.
|
|
154
|
+
*/
|
|
155
|
+
const probeApiReachable = (
|
|
156
|
+
context: string,
|
|
157
|
+
kubeconfig: string | undefined,
|
|
158
|
+
timeoutMs: number,
|
|
159
|
+
) =>
|
|
160
|
+
Effect.gen(function* () {
|
|
161
|
+
if (timeoutMs <= 0) {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const probe = ChildProcess.make(
|
|
166
|
+
"kubectl",
|
|
167
|
+
buildApiProbeArgs(kubeconfig, context, timeoutMs),
|
|
168
|
+
{
|
|
169
|
+
stdout: "pipe",
|
|
170
|
+
stderr: "pipe",
|
|
171
|
+
},
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const result = yield* Effect.scoped(
|
|
175
|
+
Effect.gen(function* () {
|
|
176
|
+
const process = yield* executor.spawn(probe);
|
|
177
|
+
return yield* collectProcessOutput(process);
|
|
178
|
+
}),
|
|
179
|
+
).pipe(Effect.catch(() => Effect.succeed({ stdout: "", stderr: "", exitCode: 1 })));
|
|
180
|
+
|
|
181
|
+
return result.exitCode === 0;
|
|
182
|
+
});
|
|
183
|
+
|
|
149
184
|
const resolveContext = Effect.fn("K8sService.resolveContext")(function* (
|
|
150
185
|
profile: string | undefined,
|
|
151
186
|
k8sConfig: K8sConfig,
|
|
@@ -223,12 +258,23 @@ export class K8sService extends Context.Service<
|
|
|
223
258
|
) {
|
|
224
259
|
const k8sConfig = yield* requireK8sConfig(profile);
|
|
225
260
|
const timeoutMs = k8sConfig.timeoutMs ?? 60000;
|
|
261
|
+
const apiProbeTimeoutMs = k8sConfig.apiProbeTimeoutMs ?? 2000;
|
|
226
262
|
return yield* runWithProfilePrerequisites(
|
|
227
263
|
config ?? {},
|
|
228
264
|
k8sConfig,
|
|
229
265
|
runPrerequisiteCommand,
|
|
230
266
|
Effect.gen(function* () {
|
|
231
267
|
const { context, kubeconfig } = yield* resolveContext(profile, k8sConfig);
|
|
268
|
+
|
|
269
|
+
const reachable = yield* probeApiReachable(context, kubeconfig, apiProbeTimeoutMs);
|
|
270
|
+
if (!reachable) {
|
|
271
|
+
return yield* new K8sContextError({
|
|
272
|
+
message: `Kubernetes API server (${k8sConfig.clusterId}) not reachable within ${apiProbeTimeoutMs}ms. VPN likely not connected, or the cluster API is degraded.`,
|
|
273
|
+
clusterId: k8sConfig.clusterId,
|
|
274
|
+
hint: "Check the VPN connection and cluster health, then retry. Set kubernetes.apiProbeTimeoutMs to 0 in agent-tools.json5 to disable this pre-flight probe.",
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
232
278
|
const fullCommand = withKubeconfig(`kubectl --context ${context} ${cmd}`, kubeconfig);
|
|
233
279
|
|
|
234
280
|
const resultOption = yield* runShellCommand(fullCommand, timeoutMs);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared kubectl API-server reachability probe, used by every VPN-gated tool
|
|
3
|
+
* (db-tool, k8s-tool) to fail fast instead of hanging on a silently unreachable cluster.
|
|
4
|
+
*
|
|
5
|
+
* Hitting `/version` via `--raw` is the cheapest authenticated round-trip; `--request-timeout`
|
|
6
|
+
* bounds it so an unreachable server (VPN down / off the office network, or the cluster API
|
|
7
|
+
* degraded) fails in ~`timeoutMs` with a clear message instead of waiting out the tool's full
|
|
8
|
+
* command/tunnel timeout.
|
|
9
|
+
*/
|
|
10
|
+
export function buildApiProbeArgs(
|
|
11
|
+
kubeconfig: string | undefined,
|
|
12
|
+
context: string,
|
|
13
|
+
timeoutMs: number,
|
|
14
|
+
): string[] {
|
|
15
|
+
return [
|
|
16
|
+
...(kubeconfig ? ["--kubeconfig", kubeconfig] : []),
|
|
17
|
+
"--context",
|
|
18
|
+
context,
|
|
19
|
+
"get",
|
|
20
|
+
"--raw=/version",
|
|
21
|
+
`--request-timeout=${timeoutMs}ms`,
|
|
22
|
+
];
|
|
23
|
+
}
|