@neat.is/core 0.10.6-dev.20261009 → 0.10.6

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.
@@ -28,7 +28,7 @@ import {
28
28
  startConnectorPolling,
29
29
  startPersistLoop,
30
30
  startStalenessLoop
31
- } from "./chunk-H2BAPN33.js";
31
+ } from "./chunk-MW2XYLCS.js";
32
32
  import {
33
33
  listProjects,
34
34
  pruneRegistry,
@@ -425,12 +425,22 @@ async function maybeStartHostedConnectors(input) {
425
425
 
426
426
  // src/connectors/kubernetes/client.ts
427
427
  import { Agent, request as httpsRequest } from "https";
428
+ var K8S_RATE_LIMIT = { capacity: 120, refillMs: 1e3 };
428
429
  function deploymentsPath(namespace) {
429
430
  return `/apis/apps/v1/namespaces/${namespace}/deployments`;
430
431
  }
431
432
  function podsPath(namespace) {
432
433
  return `/api/v1/namespaces/${namespace}/pods`;
433
434
  }
435
+ function podLogPath(namespace, podName, opts = {}) {
436
+ const params = new URLSearchParams();
437
+ if (opts.container) params.set("container", opts.container);
438
+ if (typeof opts.tailLines === "number" && Number.isFinite(opts.tailLines)) params.set("tailLines", String(Math.trunc(opts.tailLines)));
439
+ if (typeof opts.limitBytes === "number" && Number.isFinite(opts.limitBytes)) params.set("limitBytes", String(Math.trunc(opts.limitBytes)));
440
+ if (opts.previous) params.set("previous", "true");
441
+ const qs = params.toString();
442
+ return `${podsPath(namespace)}/${podName}/log${qs ? `?${qs}` : ""}`;
443
+ }
434
444
  function makeK8sFetchImpl(transport) {
435
445
  const agent = new Agent({
436
446
  ...transport.ca ? { ca: transport.ca } : {},
@@ -481,7 +491,7 @@ async function listResource(transport, namespace, path4, opts = {}) {
481
491
  { method: "GET", headers: { ...transport.token ? bearerAuthHeader(transport.token) : {}, Accept: "application/json" } },
482
492
  // accountKey: the (cluster, namespace) pair — an identifier, safe to log, the
483
493
  // rate-limit bucket for one namespace on one cluster (ADR-131).
484
- { provider: "kubernetes", accountKey: `${safeHost(transport.server)}/${namespace}`, fetchImpl }
494
+ { provider: "kubernetes", accountKey: `${safeHost(transport.server)}/${namespace}`, rateLimit: K8S_RATE_LIMIT, fetchImpl }
485
495
  );
486
496
  if (!res.ok) {
487
497
  throw new Error(`kubernetes ${path4} failed: ${res.status} ${res.statusText}`);
@@ -502,6 +512,37 @@ async function fetchDeployments(transport, namespace, opts = {}) {
502
512
  async function fetchPods(transport, namespace, opts = {}) {
503
513
  return listResource(transport, namespace, podsPath(namespace), opts);
504
514
  }
515
+ async function fetchPodLog(transport, namespace, podName, opts = {}) {
516
+ const base = opts.apiUrl ?? transport.server;
517
+ const path4 = podLogPath(namespace, podName, {
518
+ ...opts.container ? { container: opts.container } : {},
519
+ ...opts.tailLines !== void 0 ? { tailLines: opts.tailLines } : {},
520
+ ...opts.limitBytes !== void 0 ? { limitBytes: opts.limitBytes } : {},
521
+ ...opts.previous ? { previous: true } : {}
522
+ });
523
+ const url = `${base.replace(/\/$/, "")}${path4}`;
524
+ const fetchImpl = opts.fetchImpl ?? makeK8sFetchImpl(transport);
525
+ try {
526
+ const res = await junctionFetch(
527
+ url,
528
+ { method: "GET", headers: { ...transport.token ? bearerAuthHeader(transport.token) : {}, Accept: "text/plain" } },
529
+ { provider: "kubernetes", accountKey: `${safeHost(transport.server)}/${namespace}`, rateLimit: K8S_RATE_LIMIT, fetchImpl }
530
+ );
531
+ if (!res.ok) return void 0;
532
+ return await res.text();
533
+ } catch {
534
+ return void 0;
535
+ }
536
+ }
537
+ function hasLogContent(text) {
538
+ return typeof text === "string" && text.trim().length > 0;
539
+ }
540
+ async function fetchPodProcessLog(transport, namespace, podName, opts = {}) {
541
+ const previous = await fetchPodLog(transport, namespace, podName, { ...opts, previous: true });
542
+ if (hasLogContent(previous)) return previous;
543
+ const current = await fetchPodLog(transport, namespace, podName, { ...opts, previous: false });
544
+ return hasLogContent(current) ? current : void 0;
545
+ }
505
546
 
506
547
  // src/connectors/kubernetes/kubeconfig.ts
507
548
  import { readFileSync } from "fs";
@@ -604,6 +645,122 @@ function parseK8sTargetName(targetName) {
604
645
  return { serviceName, fault };
605
646
  }
606
647
 
648
+ // src/connectors/kubernetes/process-context.ts
649
+ var DEFAULT_MAX_LOG_LINES = 50;
650
+ var DEFAULT_MAX_LOG_BYTES = 2 * 1024;
651
+ var DEFAULT_MAX_ENTRIES = 64;
652
+ var DEFAULT_MAX_VALUE_CHARS = 512;
653
+ var REDACTED = "***REDACTED***";
654
+ var SECRET_KEY_RE = /TOKEN|SECRET|KEY|PASSW(?:OR)?D|PWD|(?:^|[^A-Z])PASS(?:$|[^A-Z])|CREDENTIAL|(?:^|[^A-Z])AUTH|PRIVATE|DSN|COOKIE|SESSION|SALT|SIGNATURE|CERT|CONN(?:ECTION)?_?STR/i;
655
+ function isSecretKey(name) {
656
+ return SECRET_KEY_RE.test(name);
657
+ }
658
+ var PRIVATE_KEY_BLOCK_RE = /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g;
659
+ var PRIVATE_KEY_TAIL_RE = /(?:^[A-Za-z0-9+/=]{32,}\r?\n)+-----END [A-Z ]*PRIVATE KEY-----/gm;
660
+ var URL_USERINFO_RE = /([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)([^\s/@]*)@/g;
661
+ var SECRET_PAIR_RE = /((?:^|[\s?&;,{("'])[A-Za-z0-9_.-]*?(?:passw(?:or)?d|pwd|(?<![a-z])pass(?![a-z])|secret|token|api[_-]?key|access[_-]?key|private[_-]?key|client[_-]?key|credentials?|(?<![a-z])auth(?![a-z])|(?<![a-z])dsn(?![a-z]))[A-Za-z0-9_.-]*["']?\s*[=:]\s*["']?)([^\s&;,"'})]+)/gi;
662
+ var AUTH_SCHEME_RE = /\b(Bearer|Basic|Token)\s+[A-Za-z0-9._~+/=-]{8,}/g;
663
+ var KNOWN_TOKEN_RE = /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}|\bAKIA[0-9A-Z]{16}\b|\bgh[pousr]_[A-Za-z0-9]{20,}|\bgithub_pat_[A-Za-z0-9_]{20,}|\bsk_(?:live|test)_[A-Za-z0-9]{10,}|\bxox[abprs]-[A-Za-z0-9-]{10,}|\bglpat-[A-Za-z0-9_-]{16,}|\bAIza[0-9A-Za-z_-]{30,}|\bneat_pat_[A-Za-z0-9_-]{16,}/g;
664
+ function redactText(text) {
665
+ return text.replace(PRIVATE_KEY_BLOCK_RE, REDACTED).replace(PRIVATE_KEY_TAIL_RE, REDACTED).replace(URL_USERINFO_RE, (_m, scheme, userinfo) => {
666
+ const colon = userinfo.indexOf(":");
667
+ return colon > 0 ? `${scheme}${userinfo.slice(0, colon)}:${REDACTED}@` : `${scheme}${REDACTED}@`;
668
+ }).replace(SECRET_PAIR_RE, (_m, key) => `${key}${REDACTED}`).replace(AUTH_SCHEME_RE, (_m, scheme) => `${scheme} ${REDACTED}`).replace(KNOWN_TOKEN_RE, REDACTED);
669
+ }
670
+ function redactValue(name, value) {
671
+ if (isSecretKey(name)) return REDACTED;
672
+ return redactText(value);
673
+ }
674
+ function redactArg(arg) {
675
+ const eq = /^(--?)?([A-Za-z0-9_.-]+)=([\s\S]*)$/.exec(arg);
676
+ if (eq && isSecretKey(eq[2])) {
677
+ return `${eq[1] ?? ""}${eq[2]}=${REDACTED}`;
678
+ }
679
+ return redactText(arg);
680
+ }
681
+ function isSecretFlag(arg) {
682
+ const m = /^--?([A-Za-z0-9_.-]+)$/.exec(arg);
683
+ return m !== null && isSecretKey(m[1]);
684
+ }
685
+ function capValue(s, max = DEFAULT_MAX_VALUE_CHARS) {
686
+ return s.length > max ? `${s.slice(0, max)}\u2026` : s;
687
+ }
688
+ function describeEnvVar(ev) {
689
+ const name = typeof ev.name === "string" ? ev.name : "";
690
+ if (name.length === 0) return void 0;
691
+ if (typeof ev.value === "string") {
692
+ return `${name}=${capValue(redactValue(name, ev.value))}`;
693
+ }
694
+ const from = ev.valueFrom;
695
+ if (from) {
696
+ if (from.secretKeyRef) {
697
+ const ref = typeof from.secretKeyRef.name === "string" && from.secretKeyRef.name.length > 0 ? from.secretKeyRef.name : "<unnamed>";
698
+ return `${name}=<from secret ${ref}>`;
699
+ }
700
+ if (from.configMapKeyRef) {
701
+ const ref = typeof from.configMapKeyRef.name === "string" && from.configMapKeyRef.name.length > 0 ? from.configMapKeyRef.name : "<unnamed>";
702
+ const key = typeof from.configMapKeyRef.key === "string" && from.configMapKeyRef.key.length > 0 ? ` key ${from.configMapKeyRef.key}` : "";
703
+ return `${name}=<from configMap ${ref}${key}>`;
704
+ }
705
+ if (from.fieldRef && typeof from.fieldRef.fieldPath === "string" && from.fieldRef.fieldPath.length > 0) {
706
+ return `${name}=<from field ${from.fieldRef.fieldPath}>`;
707
+ }
708
+ if (from.resourceFieldRef && typeof from.resourceFieldRef.resource === "string" && from.resourceFieldRef.resource.length > 0) {
709
+ return `${name}=<from resource ${from.resourceFieldRef.resource}>`;
710
+ }
711
+ return `${name}=<from reference>`;
712
+ }
713
+ return `${name}=`;
714
+ }
715
+ function buildContainerEnv(container) {
716
+ const out = [];
717
+ for (const ev of container.env ?? []) {
718
+ const d = describeEnvVar(ev);
719
+ if (d !== void 0) out.push(d);
720
+ if (out.length >= DEFAULT_MAX_ENTRIES) break;
721
+ }
722
+ return out;
723
+ }
724
+ function buildContainerArgs(container) {
725
+ const raw = [];
726
+ for (const c of container.command ?? []) if (typeof c === "string") raw.push(c);
727
+ for (const a of container.args ?? []) if (typeof a === "string") raw.push(a);
728
+ const out = [];
729
+ for (let i = 0; i < raw.length && out.length < DEFAULT_MAX_ENTRIES; i++) {
730
+ const arg = raw[i];
731
+ out.push(capValue(redactArg(arg)));
732
+ const next = raw[i + 1];
733
+ if (isSecretFlag(arg) && next !== void 0 && !next.startsWith("-") && out.length < DEFAULT_MAX_ENTRIES) {
734
+ out.push(REDACTED);
735
+ i++;
736
+ }
737
+ }
738
+ return out;
739
+ }
740
+ function capProcessLog(text, maxLines = DEFAULT_MAX_LOG_LINES, maxBytes = DEFAULT_MAX_LOG_BYTES) {
741
+ const trimmed = text.replace(/\s+$/, "");
742
+ if (trimmed.length === 0) return "";
743
+ const lines = trimmed.split("\n");
744
+ const tail = lines.length > maxLines ? lines.slice(lines.length - maxLines) : lines;
745
+ let out = tail.join("\n");
746
+ if (out.length > maxBytes) out = out.slice(out.length - maxBytes);
747
+ return out;
748
+ }
749
+ function buildProcessContext(input) {
750
+ const out = {};
751
+ if (typeof input.log === "string") {
752
+ const capped = capProcessLog(redactText(input.log));
753
+ if (capped.length > 0) out.processLog = capped;
754
+ }
755
+ if (input.container) {
756
+ const args = buildContainerArgs(input.container);
757
+ if (args.length > 0) out.containerArgs = args;
758
+ const env = buildContainerEnv(input.container);
759
+ if (env.length > 0) out.containerEnv = env;
760
+ }
761
+ return out;
762
+ }
763
+
607
764
  // src/connectors/kubernetes/map.ts
608
765
  function serviceNameFor(deployment, config) {
609
766
  const name = deployment.metadata?.name ?? "";
@@ -621,6 +778,9 @@ function podsForDeployment(deployment, pods) {
621
778
  function nowIso() {
622
779
  return (/* @__PURE__ */ new Date()).toISOString();
623
780
  }
781
+ function podLogKey(podName, container) {
782
+ return container ? `${podName}\0${container}` : podName;
783
+ }
624
784
  function podLevelFault(deployment, pods) {
625
785
  const name = deployment.metadata?.name ?? "";
626
786
  const owned = podsForDeployment(deployment, pods);
@@ -638,7 +798,9 @@ function podLevelFault(deployment, pods) {
638
798
  fault: "image-pull",
639
799
  message: `Deployment ${name} cannot pull image ${image} (${reason})`,
640
800
  timestamp: pod.status?.startTime ?? nowIso(),
641
- attributes: attrs
801
+ attributes: attrs,
802
+ pod,
803
+ ...typeof cs.name === "string" && cs.name.length > 0 ? { container: cs.name } : {}
642
804
  };
643
805
  }
644
806
  if (reason === CRASH_LOOP_REASON && !crash) crash = { cs, pod };
@@ -660,11 +822,47 @@ function podLevelFault(deployment, pods) {
660
822
  fault: "crash-loop",
661
823
  message: `Deployment ${name} is crashlooping (restarts: ${restarts}); ${detail}`,
662
824
  timestamp: term?.finishedAt ?? pod.status?.startTime ?? nowIso(),
663
- attributes: attrs
825
+ attributes: attrs,
826
+ pod,
827
+ ...typeof cs.name === "string" && cs.name.length > 0 ? { container: cs.name } : {}
664
828
  };
665
829
  }
666
830
  return null;
667
831
  }
832
+ function faultedContainer(finding) {
833
+ const containers = finding.pod?.spec?.containers;
834
+ if (!Array.isArray(containers) || containers.length === 0) return void 0;
835
+ if (finding.container) {
836
+ const named2 = containers.find((c) => c.name === finding.container);
837
+ if (named2) return named2;
838
+ }
839
+ return containers.length === 1 ? containers[0] : void 0;
840
+ }
841
+ function mergeProcessContext(finding, logs) {
842
+ const container = faultedContainer(finding);
843
+ const podName = finding.pod?.metadata?.name;
844
+ const log = logs && podName ? logs.get(podLogKey(podName, finding.container)) : void 0;
845
+ if (!container && log === void 0) return;
846
+ const ctx = buildProcessContext({
847
+ ...log !== void 0 ? { log } : {},
848
+ ...container ? { container } : {}
849
+ });
850
+ if (ctx.processLog !== void 0) finding.attributes["k8s.processLog"] = ctx.processLog;
851
+ if (ctx.containerArgs !== void 0) finding.attributes["k8s.containerArgs"] = ctx.containerArgs;
852
+ if (ctx.containerEnv !== void 0) finding.attributes["k8s.containerEnv"] = ctx.containerEnv;
853
+ }
854
+ function faultedPods(deployments, pods, config) {
855
+ const out = [];
856
+ const expectedZero = config.expectedZero ? new Set(config.expectedZero) : void 0;
857
+ for (const deployment of deployments) {
858
+ const finding = classifyDeployment(deployment, pods, expectedZero);
859
+ if (!finding || finding.fault !== "crash-loop") continue;
860
+ const podName = finding.pod?.metadata?.name;
861
+ if (typeof podName !== "string" || podName.length === 0) continue;
862
+ out.push({ podName, ...finding.container ? { container: finding.container } : {} });
863
+ }
864
+ return out;
865
+ }
668
866
  function classifyDeployment(deployment, pods, expectedZero) {
669
867
  const name = deployment.metadata?.name ?? "";
670
868
  const desired = typeof deployment.spec?.replicas === "number" ? deployment.spec.replicas : 1;
@@ -692,12 +890,13 @@ function classifyDeployment(deployment, pods, expectedZero) {
692
890
  attributes: { "k8s.desiredReplicas": desired, "k8s.readyReplicas": ready }
693
891
  };
694
892
  }
695
- function mapDeploymentToSignal(deployment, pods, config) {
893
+ function mapDeploymentToSignal(deployment, pods, config, logs) {
696
894
  const name = deployment.metadata?.name;
697
895
  if (typeof name !== "string" || name.length === 0) return null;
698
896
  const expectedZero = config.expectedZero ? new Set(config.expectedZero) : void 0;
699
897
  const finding = classifyDeployment(deployment, pods, expectedZero);
700
898
  if (!finding) return null;
899
+ mergeProcessContext(finding, logs);
701
900
  const serviceName = serviceNameFor(deployment, config);
702
901
  const namespace = deployment.metadata?.namespace ?? config.namespace;
703
902
  const attributes = {
@@ -750,12 +949,12 @@ function deployStateSignal(deployment, pods, config) {
750
949
  deployState: observedDeployState(deployment, pods)
751
950
  };
752
951
  }
753
- function mapWorkloadsToSignals(deployments, pods, config) {
952
+ function mapWorkloadsToSignals(deployments, pods, config, logs) {
754
953
  const out = [];
755
954
  for (const deployment of deployments) {
756
955
  const deployState = deployStateSignal(deployment, pods, config);
757
956
  if (deployState) out.push(deployState);
758
- const incident = mapDeploymentToSignal(deployment, pods, config);
957
+ const incident = mapDeploymentToSignal(deployment, pods, config, logs);
759
958
  if (incident) out.push(incident);
760
959
  }
761
960
  return out;
@@ -852,6 +1051,8 @@ async function startK8sSubstratePolling(input) {
852
1051
  }
853
1052
 
854
1053
  // src/connectors/kubernetes/index.ts
1054
+ var PROCESS_LOG_TAIL_LINES = 200;
1055
+ var PROCESS_LOG_LIMIT_BYTES = 64 * 1024;
855
1056
  var KubernetesConnector = class {
856
1057
  constructor(config, fetchImpl) {
857
1058
  this.config = config;
@@ -872,7 +1073,24 @@ var KubernetesConnector = class {
872
1073
  fetchDeployments(transport, namespace, opts),
873
1074
  fetchPods(transport, namespace, opts)
874
1075
  ]);
875
- return mapWorkloadsToSignals(deployments, pods, this.config);
1076
+ const logs = await this.fetchFaultedPodLogs(transport, namespace, deployments, pods, opts);
1077
+ return mapWorkloadsToSignals(deployments, pods, this.config, logs);
1078
+ }
1079
+ async fetchFaultedPodLogs(transport, namespace, deployments, pods, opts) {
1080
+ const logs = /* @__PURE__ */ new Map();
1081
+ const targets = faultedPods(deployments, pods, this.config);
1082
+ await Promise.all(
1083
+ targets.map(async (t) => {
1084
+ const text = await fetchPodProcessLog(transport, namespace, t.podName, {
1085
+ ...t.container ? { container: t.container } : {},
1086
+ tailLines: PROCESS_LOG_TAIL_LINES,
1087
+ limitBytes: PROCESS_LOG_LIMIT_BYTES,
1088
+ ...opts
1089
+ });
1090
+ if (typeof text === "string" && text.length > 0) logs.set(podLogKey(t.podName, t.container), text);
1091
+ })
1092
+ );
1093
+ return logs;
876
1094
  }
877
1095
  };
878
1096
  function createKubernetesConnector(graph, config, fetchImpl) {
@@ -1802,4 +2020,4 @@ export {
1802
2020
  resolveHost,
1803
2021
  startDaemon
1804
2022
  };
1805
- //# sourceMappingURL=chunk-ECBRGTB2.js.map
2023
+ //# sourceMappingURL=chunk-OKM4N7GH.js.map