Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | import * as k8s from "@kubernetes/client-node";
import { readFileSync } from "node:fs";
/**
* Cached self-pod introspection result. Queried once on first execute(),
* then reused for all subsequent Job builds so every Job inherits the
* Deployment's image, imagePullSecrets, DNS config, and PVC claim.
*/
export interface SelfPodSecretVolume {
volumeName: string;
secretName: string;
mountPath: string;
defaultMode: number | undefined;
}
export interface SelfPodInfo {
namespace: string;
image: string;
imagePullSecrets: Array<{ name: string }>;
dnsConfig: k8s.V1PodDNSConfig | undefined;
pvcClaimName: string | null;
secretVolumes: SelfPodSecretVolume[];
/** Env vars inherited from the Deployment container. */
inheritedEnv: Record<string, string>;
}
/** Keys forwarded from the Deployment container env into Job pods. */
const INHERITED_ENV_KEYS = [
"CLAUDE_CODE_USE_BEDROCK",
"AWS_REGION",
"AWS_BEARER_TOKEN_BEDROCK",
"ANTHROPIC_API_KEY",
"OPENAI_API_KEY",
"PAPERCLIP_API_URL",
];
let cachedSelfPod: SelfPodInfo | null = null;
/**
* Cache keyed by kubeconfig path (empty string = in-cluster).
* Supports multiple agents with different kubeconfigs.
*/
const kcCache = new Map<string, k8s.KubeConfig>();
function getKubeConfig(kubeconfigPath?: string): k8s.KubeConfig {
const key = kubeconfigPath ?? "";
let kc = kcCache.get(key);
if (!kc) {
kc = new k8s.KubeConfig();
if (kubeconfigPath) {
kc.loadFromFile(kubeconfigPath);
} else {
kc.loadFromCluster();
}
kcCache.set(key, kc);
}
return kc;
}
export function getBatchApi(kubeconfigPath?: string): k8s.BatchV1Api {
return getKubeConfig(kubeconfigPath).makeApiClient(k8s.BatchV1Api);
}
export function getCoreApi(kubeconfigPath?: string): k8s.CoreV1Api {
return getKubeConfig(kubeconfigPath).makeApiClient(k8s.CoreV1Api);
}
export function getAuthzApi(kubeconfigPath?: string): k8s.AuthorizationV1Api {
return getKubeConfig(kubeconfigPath).makeApiClient(k8s.AuthorizationV1Api);
}
export function getLogApi(kubeconfigPath?: string): k8s.Log {
return new k8s.Log(getKubeConfig(kubeconfigPath));
}
/**
* Read the current pod's namespace. Checks (in order):
* 1. PAPERCLIP_NAMESPACE env var (set explicitly in Deployment)
* 2. Service account namespace file (standard in-cluster path)
* 3. POD_NAMESPACE env var (Downward API convention)
* Falls back to "default" only if none of the above are available.
*/
function readInClusterNamespace(): string {
const fromEnv = process.env.PAPERCLIP_NAMESPACE ?? process.env.POD_NAMESPACE;
if (fromEnv?.trim()) return fromEnv.trim();
try {
return readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/namespace", "utf-8").trim();
} catch {
return "default";
}
}
/**
* Query the K8s API for our own pod spec and cache the result.
* Extracts image, imagePullSecrets, dnsConfig, PVC claim name,
* and environment variables to forward to Job pods.
*/
export async function getSelfPodInfo(kubeconfigPath?: string): Promise<SelfPodInfo> {
if (cachedSelfPod) return cachedSelfPod;
const hostname = process.env.HOSTNAME;
if (!hostname) {
throw new Error("claude_k8s: HOSTNAME env var not set — cannot introspect running pod");
}
const namespace = readInClusterNamespace();
const coreApi = getCoreApi(kubeconfigPath);
const pod = await coreApi.readNamespacedPod({ name: hostname, namespace });
const spec = pod.spec;
if (!spec) {
throw new Error(`claude_k8s: pod ${hostname} has no spec`);
}
const mainContainer = spec.containers[0];
if (!mainContainer?.image) {
throw new Error(`claude_k8s: pod ${hostname} has no container image`);
}
// Find PVC claim name from volumes mounted at /paperclip
let pvcClaimName: string | null = null;
const dataMount = mainContainer.volumeMounts?.find(
(vm) => vm.mountPath === "/paperclip",
);
if (dataMount) {
const volume = spec.volumes?.find((v) => v.name === dataMount.name);
pvcClaimName = volume?.persistentVolumeClaim?.claimName ?? null;
}
// Discover secret volumes mounted on the main container
const secretVolumes: SelfPodSecretVolume[] = [];
for (const vm of mainContainer.volumeMounts ?? []) {
const vol = spec.volumes?.find((v) => v.name === vm.name);
if (vol?.secret?.secretName) {
secretVolumes.push({
volumeName: vm.name,
secretName: vol.secret.secretName,
mountPath: vm.mountPath,
defaultMode: vol.secret.defaultMode,
});
}
}
// Collect inherited env vars from process.env (these came from the Deployment spec)
const inheritedEnv: Record<string, string> = {};
for (const key of INHERITED_ENV_KEYS) {
const value = process.env[key];
if (value !== undefined) {
inheritedEnv[key] = value;
}
}
cachedSelfPod = {
namespace,
image: mainContainer.image,
imagePullSecrets: (spec.imagePullSecrets ?? []).map((s) => ({
name: s.name ?? "",
})).filter((s) => s.name.length > 0),
dnsConfig: spec.dnsConfig,
pvcClaimName,
secretVolumes,
inheritedEnv,
};
return cachedSelfPod;
}
/** Reset cached state — useful for tests. */
export function resetCache(): void {
kcCache.clear();
cachedSelfPod = null;
}
|