fix: use k8s api inside branch follower

This commit is contained in:
Codex
2026-07-03 07:17:30 +00:00
parent 1c9904bbd4
commit 6f532b34a9
+84 -8
View File
@@ -1095,8 +1095,8 @@ function readNativeObjectBundle(registry: BranchFollowerRegistry, follower: Foll
const argo = native.argo;
const runtime = native.runtime;
const workloadCommands = (runtime?.workloads ?? []).map((workload, index) => {
const resource = workload.kind === "Deployment" ? "deployment" : "statefulset";
return `emit_json ${shQuote(`workload${index}`)} kubectl -n ${shQuote(runtime?.namespace ?? follower.target.namespace)} get ${resource} ${shQuote(workload.name)} -o json`;
const resource = workload.kind === "Deployment" ? "deployments" : "statefulsets";
return `emit_kube_json ${shQuote(`workload${index}`)} ${shQuote(`/apis/apps/v1/namespaces/${runtime?.namespace ?? follower.target.namespace}/${resource}/${workload.name}`)}`;
});
const script = [
"set +e",
@@ -1155,13 +1155,34 @@ function readNativeObjectBundle(registry: BranchFollowerRegistry, follower: Foll
"}",
"console.log(JSON.stringify(output));",
"NODE_COMPACT",
"emit_json() {",
"cat >\"$tmpdir/kube-get.mjs\" <<'NODE_KUBE_GET'",
"import { readFileSync } from 'node:fs';",
"import https from 'node:https';",
"const path = process.argv[2];",
"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');",
"const req = https.request({ host, port, path, method: 'GET', ca, headers: { authorization: `Bearer ${token}` } }, (res) => {",
" let body = '';",
" res.setEncoding('utf8');",
" res.on('data', (chunk) => { body += chunk; });",
" res.on('end', () => {",
" if ((res.statusCode || 0) >= 200 && (res.statusCode || 0) < 300) { process.stdout.write(body); process.exit(0); }",
" process.stderr.write(body || `kube api status ${res.statusCode}`);",
" process.exit(1);",
" });",
"});",
"req.on('error', (error) => { process.stderr.write(error?.message || String(error)); process.exit(1); });",
"req.end();",
"NODE_KUBE_GET",
"emit_kube_json() {",
" key=\"$1\"",
" shift",
" path=\"$2\"",
" raw=\"$tmpdir/$key.raw\"",
" out=\"$tmpdir/$key.out\"",
" err=\"$tmpdir/$key.err\"",
" if \"$@\" >\"$raw\" 2>\"$err\" && node \"$tmpdir/compact-native-object.mjs\" \"$key\" <\"$raw\" >\"$out\" 2>>\"$err\"; then",
" if node \"$tmpdir/kube-get.mjs\" \"$path\" >\"$raw\" 2>\"$err\" && node \"$tmpdir/compact-native-object.mjs\" \"$key\" <\"$raw\" >\"$out\" 2>>\"$err\"; then",
" printf 'UNIDESK_NATIVE_JSON\\t%s\\t' \"$key\"",
" base64 \"$out\" | tr -d '\\n'",
" printf '\\n'",
@@ -1186,7 +1207,7 @@ function readNativeObjectBundle(registry: BranchFollowerRegistry, follower: Foll
"if [ -d \"$repo_path/objects\" ]; then",
" source_commit=$(git --git-dir=\"$repo_path\" rev-parse --verify \"refs/heads/$branch^{commit}\" 2>>\"$source_err\" | head -n 1 | tr -d '\\r' || true)",
"else",
" source_commit=$(kubectl -n \"$mirror_ns\" exec \"deploy/$mirror_deploy\" -- env REPO_PATH=\"$repo_path\" BRANCH=\"$branch\" sh -lc 'git --git-dir=\"$REPO_PATH\" rev-parse --verify \"refs/heads/$BRANCH^{commit}\"' 2>>\"$source_err\" | head -n 1 | tr -d '\\r' || true)",
" printf 'formal controller/job must mount k8s git-mirror cache at %s; fallback exec is disabled\\n' \"$repo_path\" >>\"$source_err\"",
"fi",
"case \"$source_commit\" in",
" [0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f])",
@@ -1205,12 +1226,12 @@ function readNativeObjectBundle(registry: BranchFollowerRegistry, follower: Foll
: [
"if [ -n \"$source_commit\" ]; then",
" sha12=$(printf '%s' \"$source_commit\" | cut -c1-12)",
` emit_json pipelineRun kubectl -n ${shQuote(tekton.namespace)} get pipelinerun ${shQuote(`${tekton.pipelineRunPrefix}-`)}"$sha12" -o json`,
` emit_kube_json pipelineRun ${shQuote(`/apis/tekton.dev/v1/namespaces/${tekton.namespace}/pipelineruns/${tekton.pipelineRunPrefix}-`)}"$sha12"`,
"fi",
].join("\n"),
argo === null
? "true"
: `emit_json argoApplication kubectl -n ${shQuote(argo.namespace)} get application ${shQuote(argo.application)} -o json`,
: `emit_kube_json argoApplication ${shQuote(`/apis/argoproj.io/v1alpha1/namespaces/${argo.namespace}/applications/${argo.application}`)}`,
...workloadCommands,
"exit 0",
].join("\n");
@@ -1533,6 +1554,61 @@ function runKubeScript(registry: BranchFollowerRegistry, options: ParsedOptions,
function writeFollowerState(registry: BranchFollowerRegistry, state: FollowerState, options: ParsedOptions): CommandResult {
const json = JSON.stringify(state);
const dataPatch = JSON.stringify({ data: { [state.id]: json, _updatedAt: new Date().toISOString(), _specRef: SPEC_REF } });
if (options.controller) {
const patchBase64 = Buffer.from(dataPatch, "utf8").toString("base64");
const createBase64 = Buffer.from(JSON.stringify({
metadata: {
name: registry.controller.stateConfigMapName,
namespace: registry.controller.namespace,
},
data: {
_createdAt: new Date().toISOString(),
_specRef: SPEC_REF,
},
}), "utf8").toString("base64");
const script = [
"set -eu",
`PATCH_B64=${shQuote(patchBase64)}`,
`CREATE_B64=${shQuote(createBase64)}`,
`NAMESPACE=${shQuote(registry.controller.namespace)}`,
`CONFIGMAP=${shQuote(registry.controller.stateConfigMapName)}`,
"export PATCH_B64 CREATE_B64 NAMESPACE CONFIGMAP",
"node <<'NODE_KUBE_PATCH'",
"import { readFileSync } from 'node:fs';",
"import https from 'node:https';",
"const host = process.env.KUBERNETES_SERVICE_HOST;",
"const port = Number(process.env.KUBERNETES_SERVICE_PORT || '443');",
"const namespace = process.env.NAMESPACE;",
"const name = process.env.CONFIGMAP;",
"const token = readFileSync('/var/run/secrets/kubernetes.io/serviceaccount/token', 'utf8').trim();",
"const ca = readFileSync('/var/run/secrets/kubernetes.io/serviceaccount/ca.crt');",
"const patch = Buffer.from(process.env.PATCH_B64 || '', 'base64').toString('utf8');",
"const create = Buffer.from(process.env.CREATE_B64 || '', 'base64').toString('utf8');",
"function request(method, path, body, contentType) {",
" return new Promise((resolve, reject) => {",
" const req = https.request({ host, port, path, method, ca, headers: { authorization: `Bearer ${token}`, ...(body ? { 'content-type': contentType || 'application/json', 'content-length': Buffer.byteLength(body) } : {}) } }, (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 (body) req.write(body);",
" req.end();",
" });",
"}",
"const base = `/api/v1/namespaces/${namespace}/configmaps`;",
"let result = await request('PATCH', `${base}/${name}`, patch, 'application/merge-patch+json');",
"if (result.status === 404) {",
" const created = await request('POST', base, create, 'application/json');",
" if (created.status < 200 || created.status >= 300) { process.stderr.write(created.text); process.exit(1); }",
" result = await request('PATCH', `${base}/${name}`, patch, 'application/merge-patch+json');",
"}",
"if (result.status < 200 || result.status >= 300) { process.stderr.write(result.text); process.exit(1); }",
"NODE_KUBE_PATCH",
].join("\n");
return runKubeScript(registry, options, script, "", 10_000);
}
const script = [
"set -eu",
`kubectl -n ${shQuote(registry.controller.namespace)} create configmap ${shQuote(registry.controller.stateConfigMapName)} --from-literal=_createdAt="$(date -Iseconds)" --dry-run=client -o yaml | kubectl apply -f - >/dev/null`,