diff --git a/scripts/src/cicd.ts b/scripts/src/cicd.ts index 16610ebb..7f62da88 100644 --- a/scripts/src/cicd.ts +++ b/scripts/src/cicd.ts @@ -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`,