@neat.is/core 0.9.11 → 0.9.12
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/dist/{chunk-3UAUMPIY.js → chunk-RBZNXA5L.js} +591 -460
- package/dist/chunk-RBZNXA5L.js.map +1 -0
- package/dist/{chunk-UNO3X6ZV.js → chunk-WE3AFQYL.js} +468 -19
- package/dist/chunk-WE3AFQYL.js.map +1 -0
- package/dist/cli.cjs +1108 -951
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/index.cjs +1229 -645
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +1245 -661
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -2
- package/dist/server.cjs +661 -534
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-3UAUMPIY.js.map +0 -1
- package/dist/chunk-UNO3X6ZV.js.map +0 -1
|
@@ -2,10 +2,12 @@ import {
|
|
|
2
2
|
DEFAULT_PROJECT,
|
|
3
3
|
Projects,
|
|
4
4
|
attachGraphToEventBus,
|
|
5
|
+
bearerAuthHeader,
|
|
5
6
|
buildApi,
|
|
6
7
|
extractFromDirectory,
|
|
7
8
|
getGraph,
|
|
8
9
|
handleSpan,
|
|
10
|
+
junctionFetch,
|
|
9
11
|
listProjects,
|
|
10
12
|
loadGraphFromDisk,
|
|
11
13
|
makeErrorSpanWriter,
|
|
@@ -13,14 +15,17 @@ import {
|
|
|
13
15
|
pruneRegistry,
|
|
14
16
|
registryPath,
|
|
15
17
|
resetGraph,
|
|
18
|
+
resolveCredential,
|
|
19
|
+
resolveFusedServiceId,
|
|
16
20
|
saveGraphToDisk,
|
|
17
21
|
setStatus,
|
|
22
|
+
startConnectorPollLoop,
|
|
18
23
|
startConnectorPolling,
|
|
19
24
|
startPersistLoop,
|
|
20
25
|
startStalenessLoop,
|
|
21
26
|
touchLastSeen,
|
|
22
27
|
writeAtomically
|
|
23
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-RBZNXA5L.js";
|
|
24
29
|
import {
|
|
25
30
|
assertBindAuthority,
|
|
26
31
|
buildOtelReceiver,
|
|
@@ -36,12 +41,441 @@ import {
|
|
|
36
41
|
unlinkSync,
|
|
37
42
|
writeFileSync
|
|
38
43
|
} from "fs";
|
|
39
|
-
import
|
|
44
|
+
import path3 from "path";
|
|
40
45
|
import { createRequire } from "module";
|
|
41
46
|
|
|
47
|
+
// src/connectors/kubernetes/client.ts
|
|
48
|
+
import { Agent, request as httpsRequest } from "https";
|
|
49
|
+
function deploymentsPath(namespace) {
|
|
50
|
+
return `/apis/apps/v1/namespaces/${namespace}/deployments`;
|
|
51
|
+
}
|
|
52
|
+
function podsPath(namespace) {
|
|
53
|
+
return `/api/v1/namespaces/${namespace}/pods`;
|
|
54
|
+
}
|
|
55
|
+
function makeK8sFetchImpl(transport) {
|
|
56
|
+
const agent = new Agent({
|
|
57
|
+
...transport.ca ? { ca: transport.ca } : {},
|
|
58
|
+
...transport.clientCert ? { cert: transport.clientCert } : {},
|
|
59
|
+
...transport.clientKey ? { key: transport.clientKey } : {},
|
|
60
|
+
rejectUnauthorized: !transport.insecureSkipTlsVerify
|
|
61
|
+
});
|
|
62
|
+
return ((url, init) => new Promise((resolve, reject) => {
|
|
63
|
+
const u = new URL(String(url));
|
|
64
|
+
const req = httpsRequest(
|
|
65
|
+
u,
|
|
66
|
+
{
|
|
67
|
+
method: (init?.method ?? "GET").toUpperCase(),
|
|
68
|
+
headers: init?.headers ?? {},
|
|
69
|
+
agent
|
|
70
|
+
},
|
|
71
|
+
(res) => {
|
|
72
|
+
const chunks = [];
|
|
73
|
+
res.on("data", (c) => chunks.push(c));
|
|
74
|
+
res.on("end", () => {
|
|
75
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
76
|
+
const status = res.statusCode ?? 0;
|
|
77
|
+
resolve({
|
|
78
|
+
ok: status >= 200 && status < 300,
|
|
79
|
+
status,
|
|
80
|
+
statusText: res.statusMessage ?? "",
|
|
81
|
+
json: async () => JSON.parse(body),
|
|
82
|
+
text: async () => body
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
req.on("error", reject);
|
|
88
|
+
const signal = init?.signal;
|
|
89
|
+
if (signal) {
|
|
90
|
+
if (signal.aborted) req.destroy(new Error("aborted"));
|
|
91
|
+
else signal.addEventListener("abort", () => req.destroy(new Error("aborted")), { once: true });
|
|
92
|
+
}
|
|
93
|
+
req.end();
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
async function listResource(transport, namespace, path4, opts = {}) {
|
|
97
|
+
const base = opts.apiUrl ?? transport.server;
|
|
98
|
+
const url = `${base.replace(/\/$/, "")}${path4}`;
|
|
99
|
+
const fetchImpl = opts.fetchImpl ?? makeK8sFetchImpl(transport);
|
|
100
|
+
const res = await junctionFetch(
|
|
101
|
+
url,
|
|
102
|
+
{ method: "GET", headers: { ...transport.token ? bearerAuthHeader(transport.token) : {}, Accept: "application/json" } },
|
|
103
|
+
// accountKey: the (cluster, namespace) pair — an identifier, safe to log, the
|
|
104
|
+
// rate-limit bucket for one namespace on one cluster (ADR-131).
|
|
105
|
+
{ provider: "kubernetes", accountKey: `${safeHost(transport.server)}/${namespace}`, fetchImpl }
|
|
106
|
+
);
|
|
107
|
+
if (!res.ok) {
|
|
108
|
+
throw new Error(`kubernetes ${path4} failed: ${res.status} ${res.statusText}`);
|
|
109
|
+
}
|
|
110
|
+
const json = await res.json();
|
|
111
|
+
return Array.isArray(json.items) ? json.items : [];
|
|
112
|
+
}
|
|
113
|
+
function safeHost(server) {
|
|
114
|
+
try {
|
|
115
|
+
return new URL(server).host;
|
|
116
|
+
} catch {
|
|
117
|
+
return "cluster";
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function fetchDeployments(transport, namespace, opts = {}) {
|
|
121
|
+
return listResource(transport, namespace, deploymentsPath(namespace), opts);
|
|
122
|
+
}
|
|
123
|
+
async function fetchPods(transport, namespace, opts = {}) {
|
|
124
|
+
return listResource(transport, namespace, podsPath(namespace), opts);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/connectors/kubernetes/kubeconfig.ts
|
|
128
|
+
import { readFileSync } from "fs";
|
|
129
|
+
import { parse as parseYaml } from "yaml";
|
|
130
|
+
function pemFrom(dataField, pathField) {
|
|
131
|
+
if (typeof dataField === "string" && dataField.length > 0) {
|
|
132
|
+
return Buffer.from(dataField, "base64").toString("utf8");
|
|
133
|
+
}
|
|
134
|
+
if (typeof pathField === "string" && pathField.length > 0) {
|
|
135
|
+
return readFileSync(pathField, "utf8");
|
|
136
|
+
}
|
|
137
|
+
return void 0;
|
|
138
|
+
}
|
|
139
|
+
function named(list, name) {
|
|
140
|
+
if (!Array.isArray(list)) return void 0;
|
|
141
|
+
const hit = list.find((e) => e && e.name === name);
|
|
142
|
+
return hit;
|
|
143
|
+
}
|
|
144
|
+
function parseKubeconfig(kubeconfig) {
|
|
145
|
+
const looksInline = /\n/.test(kubeconfig) || /(^|\s)clusters\s*:/.test(kubeconfig);
|
|
146
|
+
const text = looksInline ? kubeconfig : readFileSync(kubeconfig, "utf8");
|
|
147
|
+
let doc;
|
|
148
|
+
try {
|
|
149
|
+
doc = parseYaml(text);
|
|
150
|
+
} catch {
|
|
151
|
+
throw new Error("kubernetes connector: kubeconfig is not valid YAML");
|
|
152
|
+
}
|
|
153
|
+
if (!doc || typeof doc !== "object") {
|
|
154
|
+
throw new Error("kubernetes connector: kubeconfig is empty or malformed");
|
|
155
|
+
}
|
|
156
|
+
const currentContext = doc["current-context"];
|
|
157
|
+
if (typeof currentContext !== "string" || currentContext.length === 0) {
|
|
158
|
+
throw new Error("kubernetes connector: kubeconfig has no current-context");
|
|
159
|
+
}
|
|
160
|
+
const ctxEntry = named(doc["contexts"], currentContext);
|
|
161
|
+
const ctx = ctxEntry?.context;
|
|
162
|
+
if (!ctx) {
|
|
163
|
+
throw new Error(`kubernetes connector: kubeconfig context "${currentContext}" not found`);
|
|
164
|
+
}
|
|
165
|
+
const clusterEntry = named(doc["clusters"], String(ctx["cluster"] ?? ""));
|
|
166
|
+
const cluster = clusterEntry?.cluster;
|
|
167
|
+
if (!cluster || typeof cluster["server"] !== "string") {
|
|
168
|
+
throw new Error("kubernetes connector: kubeconfig current context has no cluster server");
|
|
169
|
+
}
|
|
170
|
+
const userEntry = named(doc["users"], String(ctx["user"] ?? ""));
|
|
171
|
+
const user = userEntry?.user ?? {};
|
|
172
|
+
const transport = {
|
|
173
|
+
server: cluster["server"],
|
|
174
|
+
insecureSkipTlsVerify: cluster["insecure-skip-tls-verify"] === true
|
|
175
|
+
};
|
|
176
|
+
const ca = pemFrom(cluster["certificate-authority-data"], cluster["certificate-authority"]);
|
|
177
|
+
if (ca) transport.ca = ca;
|
|
178
|
+
if (typeof user["token"] === "string" && user["token"].length > 0) transport.token = user["token"];
|
|
179
|
+
const clientCert = pemFrom(user["client-certificate-data"], user["client-certificate"]);
|
|
180
|
+
const clientKey = pemFrom(user["client-key-data"], user["client-key"]);
|
|
181
|
+
if (clientCert) transport.clientCert = clientCert;
|
|
182
|
+
if (clientKey) transport.clientKey = clientKey;
|
|
183
|
+
return transport;
|
|
184
|
+
}
|
|
185
|
+
function resolveK8sTransport(creds, config) {
|
|
186
|
+
if (creds.kubeconfig) return parseKubeconfig(creds.kubeconfig);
|
|
187
|
+
if (!config.apiServerUrl) {
|
|
188
|
+
throw new Error("kubernetes connector: options.apiServerUrl is required with a token credential");
|
|
189
|
+
}
|
|
190
|
+
const transport = {
|
|
191
|
+
server: config.apiServerUrl,
|
|
192
|
+
insecureSkipTlsVerify: config.insecureSkipTlsVerify === true
|
|
193
|
+
};
|
|
194
|
+
if (creds.token) transport.token = creds.token;
|
|
195
|
+
if (config.caCert) transport.ca = config.caCert;
|
|
196
|
+
return transport;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/connectors/kubernetes/types.ts
|
|
200
|
+
function readK8sCredentials(raw) {
|
|
201
|
+
const token = typeof raw["token"] === "string" && raw["token"].length > 0 ? raw["token"] : void 0;
|
|
202
|
+
const kubeconfig = typeof raw["kubeconfig"] === "string" && raw["kubeconfig"].length > 0 ? raw["kubeconfig"] : void 0;
|
|
203
|
+
if (!token && !kubeconfig) {
|
|
204
|
+
throw new Error("kubernetes connector: credentials must carry a token or a kubeconfig");
|
|
205
|
+
}
|
|
206
|
+
const out = {};
|
|
207
|
+
if (token) out.token = token;
|
|
208
|
+
if (kubeconfig) out.kubeconfig = kubeconfig;
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
var IMAGE_PULL_REASONS = /* @__PURE__ */ new Set(["ImagePullBackOff", "ErrImagePull", "InvalidImageName"]);
|
|
212
|
+
var CRASH_LOOP_REASON = "CrashLoopBackOff";
|
|
213
|
+
var FIELD_SEP = "\0";
|
|
214
|
+
var K8S_TARGET_KIND = "k8s-workload";
|
|
215
|
+
function packK8sTargetName(identity) {
|
|
216
|
+
return [identity.serviceName, identity.fault].join(FIELD_SEP);
|
|
217
|
+
}
|
|
218
|
+
function parseK8sTargetName(targetName) {
|
|
219
|
+
const sep = targetName.indexOf(FIELD_SEP);
|
|
220
|
+
if (sep === -1) return null;
|
|
221
|
+
const serviceName = targetName.slice(0, sep);
|
|
222
|
+
const fault = targetName.slice(sep + 1);
|
|
223
|
+
if (!serviceName || !fault) return null;
|
|
224
|
+
return { serviceName, fault };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// src/connectors/kubernetes/map.ts
|
|
228
|
+
function serviceNameFor(deployment, config) {
|
|
229
|
+
const name = deployment.metadata?.name ?? "";
|
|
230
|
+
return config.serviceMap?.[name] ?? name;
|
|
231
|
+
}
|
|
232
|
+
function podMatchesSelector(pod, selector) {
|
|
233
|
+
if (!selector || Object.keys(selector).length === 0) return false;
|
|
234
|
+
const labels = pod.metadata?.labels ?? {};
|
|
235
|
+
return Object.entries(selector).every(([k, v]) => labels[k] === v);
|
|
236
|
+
}
|
|
237
|
+
function podsForDeployment(deployment, pods) {
|
|
238
|
+
const selector = deployment.spec?.selector?.matchLabels;
|
|
239
|
+
return pods.filter((p) => podMatchesSelector(p, selector));
|
|
240
|
+
}
|
|
241
|
+
function nowIso() {
|
|
242
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
243
|
+
}
|
|
244
|
+
function podLevelFault(deployment, pods) {
|
|
245
|
+
const name = deployment.metadata?.name ?? "";
|
|
246
|
+
const owned = podsForDeployment(deployment, pods);
|
|
247
|
+
let crash = null;
|
|
248
|
+
for (const pod of owned) {
|
|
249
|
+
for (const cs of pod.status?.containerStatuses ?? []) {
|
|
250
|
+
const reason = cs.state?.waiting?.reason;
|
|
251
|
+
if (typeof reason !== "string") continue;
|
|
252
|
+
if (IMAGE_PULL_REASONS.has(reason)) {
|
|
253
|
+
const image = typeof cs.image === "string" ? cs.image : "unknown image";
|
|
254
|
+
const attrs = { "k8s.image": image, "k8s.waitingReason": reason };
|
|
255
|
+
const wm = cs.state?.waiting?.message;
|
|
256
|
+
if (typeof wm === "string" && wm.length > 0) attrs["k8s.waitingMessage"] = wm;
|
|
257
|
+
return {
|
|
258
|
+
fault: "image-pull",
|
|
259
|
+
message: `Deployment ${name} cannot pull image ${image} (${reason})`,
|
|
260
|
+
timestamp: pod.status?.startTime ?? nowIso(),
|
|
261
|
+
attributes: attrs
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
if (reason === CRASH_LOOP_REASON && !crash) crash = { cs, pod };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (crash) {
|
|
268
|
+
const { cs, pod } = crash;
|
|
269
|
+
const term = cs.lastState?.terminated;
|
|
270
|
+
const termReason = typeof term?.reason === "string" ? term.reason : void 0;
|
|
271
|
+
const termMsg = typeof term?.message === "string" ? term.message.trim() : void 0;
|
|
272
|
+
const restarts = typeof cs.restartCount === "number" ? cs.restartCount : 0;
|
|
273
|
+
const detail = termReason ? `last terminated: ${termReason}${termMsg ? ` \u2014 ${termMsg}` : ""}${typeof term?.exitCode === "number" ? ` (exit ${term.exitCode})` : ""}` : "no last-termination detail reported";
|
|
274
|
+
const attrs = { "k8s.waitingReason": CRASH_LOOP_REASON, "k8s.restartCount": restarts };
|
|
275
|
+
if (termReason) attrs["k8s.terminatedReason"] = termReason;
|
|
276
|
+
if (termMsg) attrs["k8s.terminatedMessage"] = termMsg;
|
|
277
|
+
if (typeof term?.exitCode === "number") attrs["k8s.exitCode"] = term.exitCode;
|
|
278
|
+
if (typeof cs.image === "string") attrs["k8s.image"] = cs.image;
|
|
279
|
+
return {
|
|
280
|
+
fault: "crash-loop",
|
|
281
|
+
message: `Deployment ${name} is crashlooping (restarts: ${restarts}); ${detail}`,
|
|
282
|
+
timestamp: term?.finishedAt ?? pod.status?.startTime ?? nowIso(),
|
|
283
|
+
attributes: attrs
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
function classifyDeployment(deployment, pods, expectedZero) {
|
|
289
|
+
const name = deployment.metadata?.name ?? "";
|
|
290
|
+
const desired = typeof deployment.spec?.replicas === "number" ? deployment.spec.replicas : 1;
|
|
291
|
+
const ready = typeof deployment.status?.readyReplicas === "number" ? deployment.status.readyReplicas : 0;
|
|
292
|
+
if (desired === 0) {
|
|
293
|
+
if (expectedZero?.has(name)) return null;
|
|
294
|
+
return {
|
|
295
|
+
fault: "scaled-to-zero",
|
|
296
|
+
message: `Deployment ${name} is scaled to 0 \u2014 no running pods (desired 0)`,
|
|
297
|
+
timestamp: nowIso(),
|
|
298
|
+
attributes: { "k8s.desiredReplicas": 0, "k8s.readyReplicas": ready }
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
if (ready >= desired) return null;
|
|
302
|
+
const podFault = podLevelFault(deployment, pods);
|
|
303
|
+
if (podFault) {
|
|
304
|
+
podFault.attributes["k8s.desiredReplicas"] = desired;
|
|
305
|
+
podFault.attributes["k8s.readyReplicas"] = ready;
|
|
306
|
+
return podFault;
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
fault: "no-ready-replicas",
|
|
310
|
+
message: `Deployment ${name} has no ready replicas (desired ${desired}, ready ${ready})`,
|
|
311
|
+
timestamp: nowIso(),
|
|
312
|
+
attributes: { "k8s.desiredReplicas": desired, "k8s.readyReplicas": ready }
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function mapDeploymentToSignal(deployment, pods, config) {
|
|
316
|
+
const name = deployment.metadata?.name;
|
|
317
|
+
if (typeof name !== "string" || name.length === 0) return null;
|
|
318
|
+
const expectedZero = config.expectedZero ? new Set(config.expectedZero) : void 0;
|
|
319
|
+
const finding = classifyDeployment(deployment, pods, expectedZero);
|
|
320
|
+
if (!finding) return null;
|
|
321
|
+
const serviceName = serviceNameFor(deployment, config);
|
|
322
|
+
const namespace = deployment.metadata?.namespace ?? config.namespace;
|
|
323
|
+
const attributes = {
|
|
324
|
+
"k8s.namespace": namespace,
|
|
325
|
+
"k8s.deployment": name,
|
|
326
|
+
"k8s.fault": finding.fault,
|
|
327
|
+
...finding.attributes
|
|
328
|
+
};
|
|
329
|
+
return {
|
|
330
|
+
targetKind: K8S_TARGET_KIND,
|
|
331
|
+
targetName: packK8sTargetName({ serviceName, fault: finding.fault }),
|
|
332
|
+
// Incident-only — no edge, so no call/error count to replay.
|
|
333
|
+
callCount: 0,
|
|
334
|
+
errorCount: 0,
|
|
335
|
+
lastObservedIso: finding.timestamp,
|
|
336
|
+
incident: {
|
|
337
|
+
id: `k8s:deploy:${namespace}:${name}:${finding.fault}`,
|
|
338
|
+
timestamp: finding.timestamp,
|
|
339
|
+
service: serviceName,
|
|
340
|
+
errorType: "k8s-deploy-failure",
|
|
341
|
+
errorMessage: finding.message,
|
|
342
|
+
attributes
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
function mapWorkloadsToSignals(deployments, pods, config) {
|
|
347
|
+
const out = [];
|
|
348
|
+
for (const deployment of deployments) {
|
|
349
|
+
const signal = mapDeploymentToSignal(deployment, pods, config);
|
|
350
|
+
if (signal) out.push(signal);
|
|
351
|
+
}
|
|
352
|
+
return out;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// src/connectors/kubernetes/resolve.ts
|
|
356
|
+
import { EdgeType } from "@neat.is/types";
|
|
357
|
+
var NO_ENV = "unknown";
|
|
358
|
+
function createK8sResolveTarget(graph) {
|
|
359
|
+
return (signal) => {
|
|
360
|
+
if (signal.targetKind !== K8S_TARGET_KIND) return null;
|
|
361
|
+
const identity = parseK8sTargetName(signal.targetName);
|
|
362
|
+
if (!identity) return null;
|
|
363
|
+
const { serviceName } = identity;
|
|
364
|
+
return {
|
|
365
|
+
targetNodeId: resolveFusedServiceId(graph, serviceName, NO_ENV),
|
|
366
|
+
serviceName,
|
|
367
|
+
edgeType: EdgeType.CALLS
|
|
368
|
+
};
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// src/connectors/kubernetes/substrate.ts
|
|
373
|
+
import { readFile } from "fs/promises";
|
|
374
|
+
import os from "os";
|
|
375
|
+
import path from "path";
|
|
376
|
+
function defaultHome() {
|
|
377
|
+
const override = process.env.NEAT_HOME;
|
|
378
|
+
if (override && override.length > 0) return path.resolve(override);
|
|
379
|
+
return path.join(os.homedir(), ".neat");
|
|
380
|
+
}
|
|
381
|
+
function k8sSubstrateConfigPath(home = defaultHome()) {
|
|
382
|
+
return path.join(home, "k8s.json");
|
|
383
|
+
}
|
|
384
|
+
async function readK8sSubstrateConfig(home = defaultHome()) {
|
|
385
|
+
let raw;
|
|
386
|
+
try {
|
|
387
|
+
raw = await readFile(k8sSubstrateConfigPath(home), "utf8");
|
|
388
|
+
} catch {
|
|
389
|
+
return { version: 1, deployments: [] };
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
const parsed = JSON.parse(raw);
|
|
393
|
+
if (!parsed || !Array.isArray(parsed.deployments)) return { version: 1, deployments: [] };
|
|
394
|
+
return parsed;
|
|
395
|
+
} catch {
|
|
396
|
+
return { version: 1, deployments: [] };
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
async function startK8sSubstratePolling(input) {
|
|
400
|
+
const config = await readK8sSubstrateConfig(input.home);
|
|
401
|
+
const env = input.env ?? process.env;
|
|
402
|
+
const stops = [];
|
|
403
|
+
for (const entry of config.deployments) {
|
|
404
|
+
if (entry.project !== void 0 && entry.project !== input.project) continue;
|
|
405
|
+
if (typeof entry.namespace !== "string" || entry.namespace.length === 0) {
|
|
406
|
+
input.onSkip?.(entry, "missing namespace");
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
let credentials;
|
|
410
|
+
try {
|
|
411
|
+
const resolved = resolveCredential(entry.credential, env);
|
|
412
|
+
credentials = resolved.kind === "fields" ? { ...resolved.fields } : { token: resolved.value };
|
|
413
|
+
} catch (err) {
|
|
414
|
+
input.onSkip?.(entry, err.message);
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
const cfg = {
|
|
418
|
+
namespace: entry.namespace,
|
|
419
|
+
...entry.apiServerUrl ? { apiServerUrl: entry.apiServerUrl } : {},
|
|
420
|
+
...entry.caCert ? { caCert: entry.caCert } : {},
|
|
421
|
+
...entry.insecureSkipTlsVerify ? { insecureSkipTlsVerify: true } : {},
|
|
422
|
+
...entry.serviceMap ? { serviceMap: entry.serviceMap } : {},
|
|
423
|
+
...entry.expectedZero ? { expectedZero: entry.expectedZero } : {}
|
|
424
|
+
};
|
|
425
|
+
const { connector, resolveTarget } = createKubernetesConnector(input.graph, cfg, input.fetchImpl);
|
|
426
|
+
const stop = startConnectorPollLoop(
|
|
427
|
+
connector,
|
|
428
|
+
{
|
|
429
|
+
projectDir: input.projectDir,
|
|
430
|
+
project: input.project,
|
|
431
|
+
credentials,
|
|
432
|
+
...input.errorsPath ? { errorsPath: input.errorsPath } : {}
|
|
433
|
+
},
|
|
434
|
+
input.graph,
|
|
435
|
+
resolveTarget,
|
|
436
|
+
{ connectorId: `k8s:${entry.id}`, ...entry.intervalMs ? { intervalMs: entry.intervalMs } : {} }
|
|
437
|
+
);
|
|
438
|
+
stops.push(stop);
|
|
439
|
+
}
|
|
440
|
+
return () => {
|
|
441
|
+
for (const stop of stops) stop();
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// src/connectors/kubernetes/index.ts
|
|
446
|
+
var KubernetesConnector = class {
|
|
447
|
+
constructor(config, fetchImpl) {
|
|
448
|
+
this.config = config;
|
|
449
|
+
this.fetchImpl = fetchImpl;
|
|
450
|
+
}
|
|
451
|
+
config;
|
|
452
|
+
fetchImpl;
|
|
453
|
+
provider = "kubernetes";
|
|
454
|
+
async poll(ctx) {
|
|
455
|
+
const creds = readK8sCredentials(ctx.credentials);
|
|
456
|
+
const transport = resolveK8sTransport(creds, this.config);
|
|
457
|
+
const namespace = this.config.namespace;
|
|
458
|
+
const opts = {
|
|
459
|
+
...this.config.apiUrl ? { apiUrl: this.config.apiUrl } : {},
|
|
460
|
+
...this.fetchImpl ? { fetchImpl: this.fetchImpl } : {}
|
|
461
|
+
};
|
|
462
|
+
const [deployments, pods] = await Promise.all([
|
|
463
|
+
fetchDeployments(transport, namespace, opts),
|
|
464
|
+
fetchPods(transport, namespace, opts)
|
|
465
|
+
]);
|
|
466
|
+
return mapWorkloadsToSignals(deployments, pods, this.config);
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
function createKubernetesConnector(graph, config, fetchImpl) {
|
|
470
|
+
return {
|
|
471
|
+
connector: new KubernetesConnector(config, fetchImpl),
|
|
472
|
+
resolveTarget: createK8sResolveTarget(graph)
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
42
476
|
// src/unrouted.ts
|
|
43
477
|
import { promises as fs } from "fs";
|
|
44
|
-
import
|
|
478
|
+
import path2 from "path";
|
|
45
479
|
function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new Date()) {
|
|
46
480
|
return {
|
|
47
481
|
timestamp: now.toISOString(),
|
|
@@ -51,34 +485,34 @@ function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new
|
|
|
51
485
|
};
|
|
52
486
|
}
|
|
53
487
|
async function appendUnroutedSpan(neatHome, record) {
|
|
54
|
-
const target =
|
|
488
|
+
const target = path2.join(neatHome, "errors.ndjson");
|
|
55
489
|
await fs.mkdir(neatHome, { recursive: true });
|
|
56
490
|
await fs.appendFile(target, JSON.stringify(record) + "\n", "utf8");
|
|
57
491
|
}
|
|
58
492
|
function unroutedErrorsPath(neatHome) {
|
|
59
|
-
return
|
|
493
|
+
return path2.join(neatHome, "errors.ndjson");
|
|
60
494
|
}
|
|
61
495
|
|
|
62
496
|
// src/daemon.ts
|
|
63
497
|
import { NodeType } from "@neat.is/types";
|
|
64
498
|
function daemonJsonPath(scanPath) {
|
|
65
|
-
return
|
|
499
|
+
return path3.join(scanPath, "neat-out", "daemon.json");
|
|
66
500
|
}
|
|
67
501
|
function daemonsDiscoveryDir(home) {
|
|
68
502
|
const base = home && home.length > 0 ? home : neatHomeFromEnv();
|
|
69
|
-
return
|
|
503
|
+
return path3.join(base, "daemons");
|
|
70
504
|
}
|
|
71
505
|
function daemonDiscoveryPath(project, home) {
|
|
72
|
-
return
|
|
506
|
+
return path3.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`);
|
|
73
507
|
}
|
|
74
508
|
function sanitizeDiscoveryName(project) {
|
|
75
509
|
return project.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
76
510
|
}
|
|
77
511
|
function neatHomeFromEnv() {
|
|
78
512
|
const env = process.env.NEAT_HOME;
|
|
79
|
-
if (env && env.length > 0) return
|
|
513
|
+
if (env && env.length > 0) return path3.resolve(env);
|
|
80
514
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
81
|
-
return
|
|
515
|
+
return path3.join(home, ".neat");
|
|
82
516
|
}
|
|
83
517
|
async function readDaemonRecord(scanPath) {
|
|
84
518
|
try {
|
|
@@ -153,17 +587,21 @@ function teardownSlot(slot) {
|
|
|
153
587
|
slot.stopConnectors();
|
|
154
588
|
} catch {
|
|
155
589
|
}
|
|
590
|
+
try {
|
|
591
|
+
slot.stopK8sSubstrate();
|
|
592
|
+
} catch {
|
|
593
|
+
}
|
|
156
594
|
try {
|
|
157
595
|
slot.detachEvents();
|
|
158
596
|
} catch {
|
|
159
597
|
}
|
|
160
598
|
}
|
|
161
599
|
function neatHomeFor(opts) {
|
|
162
|
-
if (opts.neatHome && opts.neatHome.length > 0) return
|
|
600
|
+
if (opts.neatHome && opts.neatHome.length > 0) return path3.resolve(opts.neatHome);
|
|
163
601
|
const env = process.env.NEAT_HOME;
|
|
164
|
-
if (env && env.length > 0) return
|
|
602
|
+
if (env && env.length > 0) return path3.resolve(env);
|
|
165
603
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
166
|
-
return
|
|
604
|
+
return path3.join(home, ".neat");
|
|
167
605
|
}
|
|
168
606
|
function routeSpanToProject(serviceName, projects) {
|
|
169
607
|
if (!serviceName) return DEFAULT_PROJECT;
|
|
@@ -215,7 +653,7 @@ function spanBelongsToSingleProject(graph, project, serviceName) {
|
|
|
215
653
|
);
|
|
216
654
|
}
|
|
217
655
|
async function bootstrapProject(entry, connectors = [], neatHome) {
|
|
218
|
-
const paths = pathsForProject(entry.name,
|
|
656
|
+
const paths = pathsForProject(entry.name, path3.join(entry.path, "neat-out"));
|
|
219
657
|
try {
|
|
220
658
|
const stat = await fs2.stat(entry.path);
|
|
221
659
|
if (!stat.isDirectory()) {
|
|
@@ -237,6 +675,8 @@ async function bootstrapProject(entry, connectors = [], neatHome) {
|
|
|
237
675
|
},
|
|
238
676
|
stopConnectors: () => {
|
|
239
677
|
},
|
|
678
|
+
stopK8sSubstrate: () => {
|
|
679
|
+
},
|
|
240
680
|
detachEvents: () => {
|
|
241
681
|
},
|
|
242
682
|
status: "broken",
|
|
@@ -269,6 +709,14 @@ async function bootstrapProject(entry, connectors = [], neatHome) {
|
|
|
269
709
|
`neatd: connector "${skipped.id}" (${skipped.provider}) skipped for project "${entry.name}" \u2014 ${reason}`
|
|
270
710
|
)
|
|
271
711
|
});
|
|
712
|
+
const stopK8sSubstrate = await startK8sSubstratePolling({
|
|
713
|
+
project: entry.name,
|
|
714
|
+
graph,
|
|
715
|
+
projectDir: entry.path,
|
|
716
|
+
errorsPath: paths.errorsPath,
|
|
717
|
+
...neatHome ? { home: neatHome } : {},
|
|
718
|
+
onSkip: (skipped, reason) => console.warn(`neatd: k8s substrate "${skipped.id}" skipped for project "${entry.name}" \u2014 ${reason}`)
|
|
719
|
+
});
|
|
272
720
|
await touchLastSeen(entry.name).catch(() => {
|
|
273
721
|
});
|
|
274
722
|
return {
|
|
@@ -279,6 +727,7 @@ async function bootstrapProject(entry, connectors = [], neatHome) {
|
|
|
279
727
|
stopPersist,
|
|
280
728
|
stopStaleness,
|
|
281
729
|
stopConnectors,
|
|
730
|
+
stopK8sSubstrate,
|
|
282
731
|
detachEvents,
|
|
283
732
|
status: "active"
|
|
284
733
|
};
|
|
@@ -335,7 +784,7 @@ async function startDaemon(opts = {}) {
|
|
|
335
784
|
const projectArg = typeof opts.project === "string" && opts.project.length > 0 ? opts.project : process.env.NEAT_PROJECT && process.env.NEAT_PROJECT.length > 0 ? process.env.NEAT_PROJECT : null;
|
|
336
785
|
const projectPathArg = opts.projectPath && opts.projectPath.length > 0 ? opts.projectPath : process.env.NEAT_PROJECT_PATH && process.env.NEAT_PROJECT_PATH.length > 0 ? process.env.NEAT_PROJECT_PATH : null;
|
|
337
786
|
const singleProject = projectArg;
|
|
338
|
-
const singleProjectPath = singleProject && projectPathArg ?
|
|
787
|
+
const singleProjectPath = singleProject && projectPathArg ? path3.resolve(projectPathArg) : null;
|
|
339
788
|
if (singleProject && !singleProjectPath) {
|
|
340
789
|
throw new Error(
|
|
341
790
|
`neatd: project "${singleProject}" given without a projectPath; pass NEAT_PROJECT_PATH alongside NEAT_PROJECT.`
|
|
@@ -350,7 +799,7 @@ async function startDaemon(opts = {}) {
|
|
|
350
799
|
);
|
|
351
800
|
}
|
|
352
801
|
}
|
|
353
|
-
const pidPath =
|
|
802
|
+
const pidPath = path3.join(home, "neatd.pid");
|
|
354
803
|
await writeAtomically(pidPath, `${process.pid}
|
|
355
804
|
`);
|
|
356
805
|
const slots = /* @__PURE__ */ new Map();
|
|
@@ -822,8 +1271,8 @@ async function startDaemon(opts = {}) {
|
|
|
822
1271
|
let registryWatcher = null;
|
|
823
1272
|
let reloadTimer = null;
|
|
824
1273
|
if (!singleProject) try {
|
|
825
|
-
const regDir =
|
|
826
|
-
const regBase =
|
|
1274
|
+
const regDir = path3.dirname(regPath);
|
|
1275
|
+
const regBase = path3.basename(regPath);
|
|
827
1276
|
registryWatcher = watch(regDir, (_eventType, filename) => {
|
|
828
1277
|
if (filename !== null && filename !== regBase) return;
|
|
829
1278
|
if (reloadTimer) clearTimeout(reloadTimer);
|
|
@@ -901,4 +1350,4 @@ export {
|
|
|
901
1350
|
resolveHost,
|
|
902
1351
|
startDaemon
|
|
903
1352
|
};
|
|
904
|
-
//# sourceMappingURL=chunk-
|
|
1353
|
+
//# sourceMappingURL=chunk-WE3AFQYL.js.map
|