fix: remove kubectl from follower state helpers

This commit is contained in:
Codex
2026-07-03 18:04:58 +00:00
parent c5f665726d
commit c94f83d3f0
6 changed files with 190 additions and 63 deletions
@@ -0,0 +1,50 @@
import { readFileSync } from "node:fs";
import https from "node:https";
const namespace = process.env.NAMESPACE || "";
const configMap = process.env.CONFIGMAP || "";
const patch = JSON.parse(Buffer.from(process.env.PATCH_B64 || "", "base64").toString("utf8"));
const host = process.env.KUBERNETES_SERVICE_HOST;
const port = Number(process.env.KUBERNETES_SERVICE_PORT || "443");
const token = readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/token", "utf8").trim();
const ca = readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt");
function request(method, path, body, contentType = "application/json") {
return new Promise((resolve, reject) => {
const headers = { authorization: `Bearer ${token}` };
const payload = body === undefined ? null : typeof body === "string" ? body : JSON.stringify(body);
if (payload !== null) {
headers["content-type"] = contentType;
headers["content-length"] = Buffer.byteLength(payload);
}
const req = https.request({ host, port, path, method, ca, headers }, (res) => {
let text = "";
res.setEncoding("utf8");
res.on("data", (chunk) => { text += chunk; });
res.on("end", () => resolve({ status: res.statusCode || 0, text }));
});
req.on("error", reject);
if (payload !== null) req.write(payload);
req.end();
});
}
const path = `/api/v1/namespaces/${encodeURIComponent(namespace)}/configmaps/${encodeURIComponent(configMap)}`;
const before = await request("GET", path);
if (before.status === 404) {
process.stdout.write(JSON.stringify({ ok: true, present: false, patched: false, reason: "state-configmap-not-found", parsedDownstreamCliOutput: false }));
process.exit(0);
}
if (before.status < 200 || before.status >= 300) throw new Error(before.text || `kube api GET configmap status ${before.status}`);
const beforeObject = JSON.parse(before.text);
const result = await request("PATCH", path, patch, "application/merge-patch+json");
if (result.status < 200 || result.status >= 300) throw new Error(result.text || `kube api PATCH configmap status ${result.status}`);
const afterObject = JSON.parse(result.text);
process.stdout.write(JSON.stringify({
ok: true,
present: true,
patched: true,
beforeResourceVersion: beforeObject?.metadata?.resourceVersion || null,
afterResourceVersion: afterObject?.metadata?.resourceVersion || null,
parsedDownstreamCliOutput: false,
}));
+38 -21
View File
@@ -1,38 +1,54 @@
import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import https from "node:https";
const namespace = process.env.NAMESPACE || "";
const configMap = process.env.CONFIGMAP || "";
const followerId = process.env.FOLLOWER_ID || "";
const specRef = process.env.SPEC_REF || "";
const stateJson = Buffer.from(process.env.STATE_B64 || "", "base64").toString("utf8");
const host = process.env.KUBERNETES_SERVICE_HOST;
const port = Number(process.env.KUBERNETES_SERVICE_PORT || "443");
const token = readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/token", "utf8").trim();
const ca = readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt");
function kubectl(args, input) {
return execFileSync("kubectl", ["-n", namespace, ...args], {
input,
encoding: "utf8",
stdio: ["pipe", "pipe", "pipe"],
function request(method, path, body, contentType = "application/json") {
return new Promise((resolve, reject) => {
const headers = { authorization: `Bearer ${token}` };
const payload = body === undefined ? null : typeof body === "string" ? body : JSON.stringify(body);
if (payload !== null) {
headers["content-type"] = contentType;
headers["content-length"] = Buffer.byteLength(payload);
}
const req = https.request({ host, port, path, method, ca, headers }, (res) => {
let text = "";
res.setEncoding("utf8");
res.on("data", (chunk) => { text += chunk; });
res.on("end", () => resolve({ status: res.statusCode || 0, text }));
});
req.on("error", reject);
if (payload !== null) req.write(payload);
req.end();
});
}
function readConfigMap() {
try {
return JSON.parse(kubectl(["get", "configmap", configMap, "-o", "json"]));
} catch (error) {
const stderr = String(error?.stderr || error?.message || "");
if (/not found/i.test(stderr)) return null;
throw error;
}
async function readConfigMap() {
const result = await request("GET", `/api/v1/namespaces/${encodeURIComponent(namespace)}/configmaps/${encodeURIComponent(configMap)}`);
if (result.status === 404) return null;
if (result.status < 200 || result.status >= 300) throw new Error(result.text || `kube api GET configmap status ${result.status}`);
return JSON.parse(result.text);
}
function ensureConfigMap() {
if (readConfigMap() !== null) return;
async function ensureConfigMap() {
if (await readConfigMap() !== null) return;
const object = {
apiVersion: "v1",
kind: "ConfigMap",
metadata: { name: configMap, namespace },
data: { _createdAt: new Date().toISOString(), _specRef: specRef },
};
kubectl(["apply", "-f", "-"], JSON.stringify(object));
const result = await request("POST", `/api/v1/namespaces/${encodeURIComponent(namespace)}/configmaps`, object);
if (result.status === 409) return;
if (result.status < 200 || result.status >= 300) throw new Error(result.text || `kube api POST configmap status ${result.status}`);
}
function stringOrNull(value) {
@@ -91,8 +107,8 @@ function preserveExistingTiming(state, existing) {
};
}
ensureConfigMap();
const current = readConfigMap();
await ensureConfigMap();
const current = await readConfigMap();
const beforeResourceVersion = stringOrNull(current?.metadata?.resourceVersion);
const beforeUpdatedAt = stringOrNull(current?.data?._updatedAt);
const currentText = current?.data?.[followerId];
@@ -106,8 +122,9 @@ const patch = {
_specRef: specRef,
},
};
kubectl(["patch", "configmap", configMap, "--type", "merge", "-p", JSON.stringify(patch)]);
const updated = readConfigMap();
const patchResult = await request("PATCH", `/api/v1/namespaces/${encodeURIComponent(namespace)}/configmaps/${encodeURIComponent(configMap)}`, patch, "application/merge-patch+json");
if (patchResult.status < 200 || patchResult.status >= 300) throw new Error(patchResult.text || `kube api PATCH configmap status ${patchResult.status}`);
const updated = await readConfigMap();
process.stdout.write(JSON.stringify({
ok: true,
followerId,
+33 -12
View File
@@ -1,9 +1,14 @@
import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import https from "node:https";
const namespace = process.env.NAMESPACE || "";
const configMap = process.env.CONFIGMAP || "";
const followerIds = parseFollowerIds(process.env.FOLLOWERS_JSON || "[]");
const maxTimingStages = Number(process.env.MAX_TIMING_STAGES || "24");
const host = process.env.KUBERNETES_SERVICE_HOST;
const port = Number(process.env.KUBERNETES_SERVICE_PORT || "443");
const token = readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/token", "utf8").trim();
const ca = readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt");
function parseFollowerIds(text) {
try {
@@ -14,18 +19,34 @@ function parseFollowerIds(text) {
}
}
function kubectlConfigMap() {
try {
const stdout = execFileSync("kubectl", ["-n", namespace, "get", "configmap", configMap, "-o", "json"], {
encoding: "utf8",
maxBuffer: 16 * 1024 * 1024,
stdio: ["ignore", "pipe", "pipe"],
function request(method, path, body, contentType = "application/json") {
return new Promise((resolve, reject) => {
const headers = { authorization: `Bearer ${token}` };
const payload = body === undefined ? null : typeof body === "string" ? body : JSON.stringify(body);
if (payload !== null) {
headers["content-type"] = contentType;
headers["content-length"] = Buffer.byteLength(payload);
}
const req = https.request({ host, port, path, method, ca, headers }, (res) => {
let text = "";
res.setEncoding("utf8");
res.on("data", (chunk) => { text += chunk; });
res.on("end", () => resolve({ status: res.statusCode || 0, text }));
});
return { ok: true, present: true, object: JSON.parse(stdout), error: "" };
req.on("error", reject);
if (payload !== null) req.write(payload);
req.end();
});
}
async function readConfigMap() {
try {
const result = await request("GET", `/api/v1/namespaces/${encodeURIComponent(namespace)}/configmaps/${encodeURIComponent(configMap)}`);
if (result.status === 404) return { ok: true, present: false, object: null, error: result.text };
if (result.status < 200 || result.status >= 300) return { ok: false, present: false, object: null, error: result.text || `kube api GET configmap status ${result.status}` };
return { ok: true, present: true, object: JSON.parse(result.text), error: "" };
} catch (error) {
const stderr = String(error?.stderr || error?.message || "");
if (/not found/i.test(stderr)) return { ok: true, present: false, object: null, error: stderr };
return { ok: false, present: false, object: null, error: stderr || "kubectl configmap read failed" };
return { ok: false, present: false, object: null, error: error?.message || String(error) };
}
}
@@ -139,7 +160,7 @@ function numberOrNull(value) {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
const result = kubectlConfigMap();
const result = await readConfigMap();
const errors = [];
const stateByFollower = {};
const valueBytes = {};