fix: render git mirror proxy from lane yaml (#1817)

This commit is contained in:
Lyon
2026-06-21 12:40:50 +08:00
committed by GitHub
parent 70d90e85eb
commit ec1380dc4e
+138 -81
View File
@@ -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" }] }
}
]
};