From ec1380dc4e6b66440769e6a32ed38b3ef0d86aa1 Mon Sep 17 00:00:00 2001 From: Lyon <88232613+pikasTech@users.noreply.github.com> Date: Sun, 21 Jun 2026 12:40:50 +0800 Subject: [PATCH] fix: render git mirror proxy from lane yaml (#1817) --- scripts/gitops-render.mjs | 219 ++++++++++++++++++++++++-------------- 1 file changed, 138 insertions(+), 81 deletions(-) diff --git a/scripts/gitops-render.mjs b/scripts/gitops-render.mjs index 735661d0..cdd13304 100644 --- a/scripts/gitops-render.mjs +++ b/scripts/gitops-render.mjs @@ -3603,6 +3603,30 @@ function uniqueStrings(values) { return Array.from(new Set(values.filter((value) => typeof value === "string" && value.length > 0))); } +function configObject(value, label) { + assert.ok(value && typeof value === "object" && !Array.isArray(value), `${label} must be an object`); + return value; +} + +function requiredConfigString(record, key, label) { + const value = record[key]; + assert.ok(typeof value === "string" && value.length > 0, `${label}.${key} must be a non-empty string`); + return value; +} + +function optionalConfigString(record, key, label) { + const value = record[key]; + if (value === undefined || value === null || value === "") return null; + assert.ok(typeof value === "string", `${label}.${key} must be a string when set`); + return value; +} + +function requiredConfigPositiveInteger(record, key, label) { + const value = record[key]; + assert.ok(Number.isInteger(value) && value > 0, `${label}.${key} must be a positive integer`); + return value; +} + function devopsInfraGitMirrorManifest(args, deploy) { const mirrorSpecs = runtimeLaneMirrorSpecs(deploy, { defaultNodeId: args.nodeId, defaultGitopsRoot: defaultOutDir }); assert.ok(mirrorSpecs.length > 0, "runtime lane git mirror requires at least one deploy.lanes entry"); @@ -3618,25 +3642,120 @@ function devopsInfraGitMirrorManifest(args, deploy) { const mirrorBranchesJson = JSON.stringify(mirrorBranches); const sourceBranchesJson = JSON.stringify(sourceBranches); const gitopsBranchesJson = JSON.stringify(gitopsBranches); + const laneConfig = configObject(configObject(deploy.lanes, "deploy.lanes")[args.lane], `deploy.lanes.${args.lane}`); + const gitMirrorConfig = configObject(laneConfig.gitMirror, `deploy.lanes.${args.lane}.gitMirror`); + const gitMirrorProxy = configObject(gitMirrorConfig.egressProxy, `deploy.lanes.${args.lane}.gitMirror.egressProxy`); + assert.equal(gitMirrorProxy.mode, "node-global", `deploy.lanes.${args.lane}.gitMirror.egressProxy.mode must be node-global`); + const gitMirrorNamespace = requiredConfigString(gitMirrorConfig, "namespace", `deploy.lanes.${args.lane}.gitMirror`); + const gitMirrorReadName = requiredConfigString(gitMirrorConfig, "serviceReadName", `deploy.lanes.${args.lane}.gitMirror`); + const gitMirrorWriteName = requiredConfigString(gitMirrorConfig, "serviceWriteName", `deploy.lanes.${args.lane}.gitMirror`); + const gitMirrorCachePvcName = requiredConfigString(gitMirrorConfig, "cachePvcName", `deploy.lanes.${args.lane}.gitMirror`); + const gitMirrorCachePvcStorage = requiredConfigString(gitMirrorConfig, "cachePvcStorage", `deploy.lanes.${args.lane}.gitMirror`); + const gitMirrorCacheHostPath = optionalConfigString(gitMirrorConfig, "cacheHostPath", `deploy.lanes.${args.lane}.gitMirror`); + const gitMirrorServicePort = requiredConfigPositiveInteger(gitMirrorConfig, "servicePort", `deploy.lanes.${args.lane}.gitMirror`); + const gitMirrorConfigMapName = requiredConfigString(gitMirrorConfig, "syncConfigMapName", `deploy.lanes.${args.lane}.gitMirror`); + const proxyNamespace = requiredConfigString(gitMirrorProxy, "namespace", `deploy.lanes.${args.lane}.gitMirror.egressProxy`); + const proxyServiceName = requiredConfigString(gitMirrorProxy, "serviceName", `deploy.lanes.${args.lane}.gitMirror.egressProxy`); + const proxyPort = requiredConfigPositiveInteger(gitMirrorProxy, "port", `deploy.lanes.${args.lane}.gitMirror.egressProxy`); + const proxyClientName = requiredConfigString(gitMirrorProxy, "clientName", `deploy.lanes.${args.lane}.gitMirror.egressProxy`); + const proxyNoProxy = Array.isArray(gitMirrorProxy.noProxy) ? gitMirrorProxy.noProxy.filter((value) => typeof value === "string" && value.length > 0).join(",") : ""; + const proxyHost = `${proxyServiceName}.${proxyNamespace}.svc.cluster.local`; + const proxyUrl = `http://${proxyHost}:${proxyPort}`; + const proxySummary = `git-mirror-egress-proxy client=${proxyClientName} mode=node-global required=true host=${proxyHost} port=${proxyPort} ssh=GIT_SSH-wrapper source=yaml`; + const proxyCommand = `ProxyCommand=node /tmp/hwlab-github-proxy-connect.mjs ${proxyHost} ${proxyPort} %h %p`; + const gitMirrorSshPrelude = `printf '%s\\n' ${shellSingleQuote(proxySummary)} >&2 +cat > /tmp/hwlab-github-proxy-connect.mjs <<'NODE_PROXY' +#!/usr/bin/env node +import net from "node:net"; +const [proxyHost, proxyPortRaw, targetHost, targetPortRaw] = process.argv.slice(2); +const proxyPort = Number.parseInt(proxyPortRaw || "", 10); +const targetPort = Number.parseInt(targetPortRaw || "", 10); +if (!proxyHost || !Number.isInteger(proxyPort) || !targetHost || !Number.isInteger(targetPort)) { + console.error("hwlab git-mirror proxy-connect: invalid ProxyCommand arguments"); + process.exit(64); +} +let settled = false; +let tunnelEstablished = false; +function finish(code, message) { + if (settled) return; + settled = true; + if (message) console.error("hwlab git-mirror proxy-connect: " + message); + process.exit(code); +} +const socket = net.createConnection({ host: proxyHost, port: proxyPort }); +let buffer = Buffer.alloc(0); +socket.setTimeout(15000, () => { socket.destroy(); finish(65, "timeout connecting via " + proxyHost + ":" + proxyPort + " to " + targetHost + ":" + targetPort); }); +socket.on("connect", () => socket.write("CONNECT " + targetHost + ":" + targetPort + " HTTP/1.1\\r\\nHost: " + targetHost + ":" + targetPort + "\\r\\nProxy-Connection: Keep-Alive\\r\\n\\r\\n")); +socket.on("error", (error) => finish(tunnelEstablished ? 69 : 66, (tunnelEstablished ? "tunnel socket error: " : "tcp error connecting to proxy: ") + (error && error.message ? error.message : String(error)))); +socket.on("close", () => { if (!tunnelEstablished) finish(68, "proxy closed before CONNECT completed via " + proxyHost + ":" + proxyPort + " to " + targetHost + ":" + targetPort); else finish(0); }); +function onData(chunk) { + buffer = Buffer.concat([buffer, chunk]); + const headerEnd = buffer.indexOf("\\r\\n\\r\\n"); + if (headerEnd === -1 && buffer.length < 8192) return; + if (headerEnd === -1) { socket.destroy(); finish(68, "proxy response header exceeded 8192 bytes before CONNECT status via " + proxyHost + ":" + proxyPort + " to " + targetHost + ":" + targetPort); return; } + const head = buffer.slice(0, headerEnd + 4).toString("latin1"); + const statusLine = head.split("\\r\\n", 1)[0] || ""; + const statusCode = Number.parseInt(statusLine.split(" ")[1] || "", 10); + if (!statusLine.startsWith("HTTP/1.") || !Number.isInteger(statusCode) || statusCode < 200 || statusCode > 299) { + const safeStatus = statusLine.replace(/[^\\x20-\\x7e]/g, "?").slice(0, 160); + socket.destroy(); + finish(67, "proxy CONNECT failed via " + proxyHost + ":" + proxyPort + " to " + targetHost + ":" + targetPort + ": " + safeStatus); + return; + } + socket.off("data", onData); + socket.setTimeout(0); + tunnelEstablished = true; + const rest = buffer.slice(headerEnd + 4); + if (rest.length) process.stdout.write(rest); + process.stdin.on("error", () => {}); + process.stdout.on("error", () => {}); + process.stdin.pipe(socket); + socket.pipe(process.stdout); +} +socket.on("data", onData); +NODE_PROXY +chmod 0700 /tmp/hwlab-github-proxy-connect.mjs +cat > /tmp/hwlab-git-ssh-proxy.sh <<'SH_PROXY' +#!/bin/sh +exec ssh -i /root/.ssh/id_rsa -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/root/.ssh/known_hosts -o ConnectTimeout=15 -o ServerAliveInterval=5 -o ServerAliveCountMax=1 -o ${shellSingleQuote(proxyCommand)} "$@" +SH_PROXY +chmod 0700 /tmp/hwlab-git-ssh-proxy.sh +export HTTP_PROXY=${shellSingleQuote(proxyUrl)} +export HTTPS_PROXY=${shellSingleQuote(proxyUrl)} +export ALL_PROXY=${shellSingleQuote(proxyUrl)} +export http_proxy=${shellSingleQuote(proxyUrl)} +export https_proxy=${shellSingleQuote(proxyUrl)} +export all_proxy=${shellSingleQuote(proxyUrl)} +export NO_PROXY=${shellSingleQuote(proxyNoProxy)} +export no_proxy=${shellSingleQuote(proxyNoProxy)} +export GIT_SSH=/tmp/hwlab-git-ssh-proxy.sh +unset GIT_SSH_COMMAND`; + const cacheVolume = gitMirrorCacheHostPath === null + ? { name: "cache", persistentVolumeClaim: { claimName: gitMirrorCachePvcName } } + : { name: "cache", hostPath: { path: gitMirrorCacheHostPath, type: "DirectoryOrCreate" } }; const labels = { - "app.kubernetes.io/name": "git-mirror", - "app.kubernetes.io/part-of": "devops-infra" + "app.kubernetes.io/part-of": "hwlab-node-control-plane", + "hwlab.pikastech.local/node": args.nodeId, + "hwlab.pikastech.local/lane": args.lane }; + const configLabels = { ...labels, "app.kubernetes.io/name": "git-mirror" }; + const readLabels = { ...labels, "app.kubernetes.io/name": gitMirrorReadName, "hwlab.pikastech.local/git-mirror-mode": "read" }; + const writeLabels = { ...labels, "app.kubernetes.io/name": gitMirrorWriteName, "hwlab.pikastech.local/git-mirror-mode": "write" }; return { apiVersion: "v1", kind: "List", items: [ - { apiVersion: "v1", kind: "Namespace", metadata: { name: "devops-infra" } }, - { + { apiVersion: "v1", kind: "Namespace", metadata: { name: gitMirrorNamespace } }, + ...(gitMirrorCacheHostPath === null ? [{ apiVersion: "v1", kind: "PersistentVolumeClaim", - metadata: { name: "git-mirror-cache", namespace: "devops-infra", labels }, - spec: { accessModes: ["ReadWriteOnce"], resources: { requests: { storage: "20Gi" } } } - }, + metadata: { name: gitMirrorCachePvcName, namespace: gitMirrorNamespace, labels: configLabels }, + spec: { accessModes: ["ReadWriteOnce"], resources: { requests: { storage: gitMirrorCachePvcStorage } } } + }] : []), { apiVersion: "v1", kind: "ConfigMap", - metadata: { name: "git-mirror-sync-script", namespace: "devops-infra", labels }, + metadata: { name: gitMirrorConfigMapName, namespace: gitMirrorNamespace, labels: configLabels }, data: { "sync.sh": `#!/bin/sh set -eu @@ -3661,38 +3780,7 @@ fi trap 'rmdir "$lock_dir" 2>/dev/null || true' EXIT INT TERM cp /git-ssh/ssh-privatekey /root/.ssh/id_rsa chmod 0400 /root/.ssh/id_rsa -cat > /tmp/hwlab-github-proxy-connect.mjs <<'NODE_PROXY' -#!/usr/bin/env node -import net from "node:net"; -const [proxyHost, proxyPortRaw, targetHost, targetPortRaw] = process.argv.slice(2); -const proxyPort = Number.parseInt(proxyPortRaw || "", 10); -const targetPort = Number.parseInt(targetPortRaw || "", 10); -if (!proxyHost || !Number.isInteger(proxyPort) || !targetHost || !Number.isInteger(targetPort)) process.exit(64); -const socket = net.createConnection({ host: proxyHost, port: proxyPort }); -let buffer = Buffer.alloc(0); -socket.setTimeout(10000, () => { socket.destroy(); process.exit(65); }); -socket.on("connect", () => socket.write("CONNECT " + targetHost + ":" + targetPort + " HTTP/1.1\\r\\nHost: " + targetHost + ":" + targetPort + "\\r\\nProxy-Connection: Keep-Alive\\r\\n\\r\\n")); -socket.on("error", () => process.exit(66)); -function onData(chunk) { - buffer = Buffer.concat([buffer, chunk]); - const headerEnd = buffer.indexOf("\\r\\n\\r\\n"); - if (headerEnd === -1 && buffer.length < 8192) return; - const head = buffer.slice(0, headerEnd + 4).toString("latin1"); - const statusLine = head.split("\\r\\n", 1)[0] || ""; - const statusCode = Number.parseInt(statusLine.split(" ")[1] || "", 10); - if (!statusLine.startsWith("HTTP/1.") || !Number.isInteger(statusCode) || statusCode < 200 || statusCode > 299) { socket.destroy(); process.exit(67); } - socket.off("data", onData); - socket.setTimeout(0); - const rest = buffer.slice(headerEnd + 4); - if (rest.length) process.stdout.write(rest); - process.stdin.pipe(socket); - socket.pipe(process.stdout); -} -socket.on("data", onData); -socket.on("close", () => process.exit(0)); -NODE_PROXY -chmod 0700 /tmp/hwlab-github-proxy-connect.mjs -export GIT_SSH_COMMAND="ssh -i /root/.ssh/id_rsa -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/root/.ssh/known_hosts -o ConnectTimeout=10 -o ServerAliveInterval=5 -o ServerAliveCountMax=1 -o ProxyCommand='node /tmp/hwlab-github-proxy-connect.mjs 127.0.0.1 10808 %h %p'" +${gitMirrorSshPrelude} if [ -d "$repo_path/objects" ] && [ -f "$repo_path/HEAD" ]; then git -C "$repo_path" remote set-url origin "$repo_url" || git -C "$repo_path" remote add origin "$repo_url" else @@ -3915,38 +4003,7 @@ trap 'rmdir "$lock_dir" 2>/dev/null || true' EXIT INT TERM mkdir -p /root/.ssh cp /git-ssh/ssh-privatekey /root/.ssh/id_rsa chmod 0400 /root/.ssh/id_rsa -cat > /tmp/hwlab-github-proxy-connect.mjs <<'NODE_PROXY' -#!/usr/bin/env node -import net from "node:net"; -const [proxyHost, proxyPortRaw, targetHost, targetPortRaw] = process.argv.slice(2); -const proxyPort = Number.parseInt(proxyPortRaw || "", 10); -const targetPort = Number.parseInt(targetPortRaw || "", 10); -if (!proxyHost || !Number.isInteger(proxyPort) || !targetHost || !Number.isInteger(targetPort)) process.exit(64); -const socket = net.createConnection({ host: proxyHost, port: proxyPort }); -let buffer = Buffer.alloc(0); -socket.setTimeout(10000, () => { socket.destroy(); process.exit(65); }); -socket.on("connect", () => socket.write("CONNECT " + targetHost + ":" + targetPort + " HTTP/1.1\\r\\nHost: " + targetHost + ":" + targetPort + "\\r\\nProxy-Connection: Keep-Alive\\r\\n\\r\\n")); -socket.on("error", () => process.exit(66)); -function onData(chunk) { - buffer = Buffer.concat([buffer, chunk]); - const headerEnd = buffer.indexOf("\\r\\n\\r\\n"); - if (headerEnd === -1 && buffer.length < 8192) return; - const head = buffer.slice(0, headerEnd + 4).toString("latin1"); - const statusLine = head.split("\\r\\n", 1)[0] || ""; - const statusCode = Number.parseInt(statusLine.split(" ")[1] || "", 10); - if (!statusLine.startsWith("HTTP/1.") || !Number.isInteger(statusCode) || statusCode < 200 || statusCode > 299) { socket.destroy(); process.exit(67); } - socket.off("data", onData); - socket.setTimeout(0); - const rest = buffer.slice(headerEnd + 4); - if (rest.length) process.stdout.write(rest); - process.stdin.pipe(socket); - socket.pipe(process.stdout); -} -socket.on("data", onData); -socket.on("close", () => process.exit(0)); -NODE_PROXY -chmod 0700 /tmp/hwlab-github-proxy-connect.mjs -export GIT_SSH_COMMAND="ssh -i /root/.ssh/id_rsa -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/root/.ssh/known_hosts -o ConnectTimeout=10 -o ServerAliveInterval=5 -o ServerAliveCountMax=1 -o ProxyCommand='node /tmp/hwlab-github-proxy-connect.mjs 127.0.0.1 10808 %h %p'" +${gitMirrorSshPrelude} /script/install-hooks.sh for gitops_branch in ${gitopsBranchArgs}; do local_head=$(git -C "$repo_path" show-ref --verify --hash "refs/heads/$gitops_branch" 2>/dev/null || true) @@ -4201,12 +4258,12 @@ function contentType(filePath) { { apiVersion: "apps/v1", kind: "Deployment", - metadata: { name: "git-mirror-http", namespace: "devops-infra", labels: { ...labels, "app.kubernetes.io/component": "read-only-http" } }, + metadata: { name: gitMirrorReadName, namespace: gitMirrorNamespace, labels: readLabels }, spec: { replicas: 1, - selector: { matchLabels: { "app.kubernetes.io/name": "git-mirror", "app.kubernetes.io/component": "read-only-http" } }, + selector: { matchLabels: { "app.kubernetes.io/name": gitMirrorReadName } }, template: { - metadata: { labels: { ...labels, "app.kubernetes.io/component": "read-only-http" } }, + metadata: { labels: readLabels }, spec: { containers: [{ name: "http", @@ -4223,8 +4280,8 @@ function contentType(filePath) { ] }], volumes: [ - { name: "cache", persistentVolumeClaim: { claimName: "git-mirror-cache" } }, - { name: "script", configMap: { name: "git-mirror-sync-script", defaultMode: 0o555 } } + cacheVolume, + { name: "script", configMap: { name: gitMirrorConfigMapName, defaultMode: 0o555 } } ] } } @@ -4233,14 +4290,14 @@ function contentType(filePath) { { apiVersion: "v1", kind: "Service", - metadata: { name: "git-mirror-http", namespace: "devops-infra", labels: { ...labels, "app.kubernetes.io/component": "read-only-http" } }, - spec: { selector: { "app.kubernetes.io/name": "git-mirror", "app.kubernetes.io/component": "read-only-http" }, ports: [{ name: "http", port: 80, targetPort: "http" }] } + metadata: { name: gitMirrorReadName, namespace: gitMirrorNamespace, labels: readLabels }, + spec: { selector: { "app.kubernetes.io/name": gitMirrorReadName }, ports: [{ name: "http", port: gitMirrorServicePort, targetPort: "http" }] } }, { apiVersion: "v1", kind: "Service", - metadata: { name: "git-mirror-write", namespace: "devops-infra", labels: { ...labels, "app.kubernetes.io/component": "write-relay" } }, - spec: { selector: { "app.kubernetes.io/name": "git-mirror", "app.kubernetes.io/component": "read-only-http" }, ports: [{ name: "http", port: 80, targetPort: "http" }] } + metadata: { name: gitMirrorWriteName, namespace: gitMirrorNamespace, labels: writeLabels }, + spec: { selector: { "app.kubernetes.io/name": gitMirrorReadName }, ports: [{ name: "http", port: gitMirrorServicePort, targetPort: "http" }] } } ] };