130 lines
8.2 KiB
TypeScript
130 lines
8.2 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { rootPath } from "./config";
|
|
import type { ControlPlaneNodeSpec, ControlPlaneTargetSpec } from "./hwlab-node-control-plane-model";
|
|
|
|
const SCRIPT_ROOT = "scripts/src/hwlab-node-control-plane";
|
|
|
|
export function gitMirrorSyncShell(node: ControlPlaneNodeSpec, target: ControlPlaneTargetSpec): string {
|
|
return renderShellTemplate("git-mirror-sync.sh", {
|
|
GIT_MIRROR_PROXY_PRELUDE: gitMirrorProxyPrelude(node, target),
|
|
});
|
|
}
|
|
|
|
export function gitMirrorFlushShell(node: ControlPlaneNodeSpec, target: ControlPlaneTargetSpec): string {
|
|
return renderShellTemplate("git-mirror-flush.sh", {
|
|
GIT_MIRROR_PROXY_PRELUDE: gitMirrorProxyPrelude(node, target),
|
|
});
|
|
}
|
|
|
|
function renderShellTemplate(fileName: string, replacements: Record<string, string>): string {
|
|
let script = readScript(fileName);
|
|
for (const [key, value] of Object.entries(replacements)) script = script.replaceAll(`__${key}__`, value.trimEnd());
|
|
if (/__[A-Z0-9_]+__/u.test(script)) throw new Error(`unresolved git-mirror script template marker in ${fileName}`);
|
|
return script.endsWith("\n") ? script : `${script}\n`;
|
|
}
|
|
|
|
function readScript(fileName: string): string {
|
|
return readFileSync(rootPath(SCRIPT_ROOT, fileName), "utf8");
|
|
}
|
|
|
|
function gitMirrorProxyPrelude(node: ControlPlaneNodeSpec, target: ControlPlaneTargetSpec): string {
|
|
const gitMirrorProxy = target.gitMirror.egressProxy;
|
|
const transport = target.gitMirror.githubTransport;
|
|
const useDirect = gitMirrorProxy?.mode === "direct";
|
|
const proxyRequired = gitMirrorProxy?.required === true;
|
|
if (!useDirect && gitMirrorProxy?.mode !== "node-global" && gitMirrorProxy?.mode !== "host-route") throw new Error(`targets.${target.id}.gitMirror.egressProxy.mode must be node-global, host-route, or direct`);
|
|
if (!useDirect && proxyRequired && node.egressProxy === null) throw new Error(`nodes.${node.id}.egressProxy is required by targets.${target.id}.gitMirror.egressProxy`);
|
|
if (!useDirect && node.egressProxy === null) throw new Error(`nodes.${node.id}.egressProxy is missing; git-mirror GitHub transport cannot use node-global proxy`);
|
|
if (!useDirect && gitMirrorProxy?.mode === "node-global" && node.egressProxy?.mode !== "k8s-service-cluster-ip") throw new Error(`targets.${target.id}.gitMirror.egressProxy.mode=node-global requires nodes.${node.id}.egressProxy.mode=k8s-service-cluster-ip`);
|
|
if (!useDirect && gitMirrorProxy?.mode === "host-route" && node.egressProxy?.mode !== "host-route") throw new Error(`targets.${target.id}.gitMirror.egressProxy.mode=host-route requires nodes.${node.id}.egressProxy.mode=host-route`);
|
|
const hostRouteUrl = !useDirect && node.egressProxy?.mode === "host-route" ? new URL(node.egressProxy.proxyUrl) : null;
|
|
const proxyHost = useDirect || node.egressProxy === null ? "" : node.egressProxy.mode === "host-route" ? hostRouteUrl!.hostname : `${node.egressProxy.serviceName}.${node.egressProxy.namespace}.svc.cluster.local`;
|
|
const proxyPort = useDirect || node.egressProxy === null ? 0 : node.egressProxy.mode === "host-route" ? Number(hostRouteUrl!.port) : node.egressProxy.port;
|
|
const proxyUrl = useDirect || node.egressProxy === null ? "" : node.egressProxy.mode === "host-route" ? node.egressProxy.proxyUrl : `http://${proxyHost}:${proxyPort}`;
|
|
const noProxy = useDirect || node.egressProxy === null ? "" : node.egressProxy.noProxy.join(",");
|
|
const proxySource = useDirect || node.egressProxy === null
|
|
? "sourceType=direct sourceRef=- sourceFingerprint=-"
|
|
: node.egressProxy.mode === "host-route"
|
|
? `sourceType=host-route hostProxyConfigRef=${node.egressProxy.hostProxyConfigRef} sourceFingerprint=-`
|
|
: `sourceType=${node.egressProxy.sourceType} sourceRef=${node.egressProxy.sourceRef} sourceFingerprint=${node.egressProxy.sourceFingerprint ?? "-"}`;
|
|
const proxySummary = useDirect
|
|
? `git-mirror-egress-proxy mode=direct required=false transport=${transport.mode} source=yaml`
|
|
: transport.mode === "https"
|
|
? `git-mirror-egress-proxy client=${node.egressProxy?.clientName} mode=${node.egressProxy?.mode} required=${proxyRequired ? "true" : "false"} host=${proxyHost} port=${proxyPort} transport=https proxy=HTTP_PROXY authSecret=${transport.tokenSecretName} authKey=${transport.tokenSecretKey} authSourceRef=${transport.tokenSourceRef} ${proxySource} source=yaml`
|
|
: `git-mirror-egress-proxy client=${node.egressProxy?.clientName} mode=${node.egressProxy?.mode} required=${proxyRequired ? "true" : "false"} host=${proxyHost} port=${proxyPort} transport=ssh ssh=GIT_SSH-wrapper ${proxySource} source=yaml`;
|
|
const common = [
|
|
`printf '%s\\n' ${shQuote(proxySummary)} >&2`,
|
|
useDirect ? "unset HTTP_PROXY HTTPS_PROXY ALL_PROXY http_proxy https_proxy all_proxy" : `export HTTP_PROXY=${shQuote(proxyUrl)}`,
|
|
useDirect ? "export NO_PROXY='*'" : `export HTTPS_PROXY=${shQuote(proxyUrl)}`,
|
|
useDirect ? "export no_proxy='*'" : `export ALL_PROXY=${shQuote(proxyUrl)}`,
|
|
useDirect ? "" : `export http_proxy=${shQuote(proxyUrl)}`,
|
|
useDirect ? "" : `export https_proxy=${shQuote(proxyUrl)}`,
|
|
useDirect ? "" : `export all_proxy=${shQuote(proxyUrl)}`,
|
|
useDirect ? "" : `export NO_PROXY=${shQuote(noProxy)}`,
|
|
useDirect ? "" : `export no_proxy=${shQuote(noProxy)}`,
|
|
`repository=${shQuote(target.source.repository)}`,
|
|
`source_branch=${shQuote(target.source.branch)}`,
|
|
`source_stage_ref_prefix=${shQuote(target.source.sourceSnapshot.stageRefPrefix.replaceAll("{branch}", target.source.branch))}`,
|
|
`gitops_branch=${shQuote(target.gitops.branch)}`,
|
|
"repo=\"/cache/${repository}.git\"",
|
|
].filter(Boolean);
|
|
if (transport.mode === "https") {
|
|
return [
|
|
...common,
|
|
"if [ -z \"${GITHUB_TOKEN:-}\" ]; then echo 'hwlab git-mirror https auth: missing GITHUB_TOKEN secret env' >&2; exit 64; fi",
|
|
"cat > /tmp/hwlab-git-askpass.sh <<'SH_ASKPASS'",
|
|
"#!/bin/sh",
|
|
"case \"$1\" in",
|
|
" *Username*) printf '%s\\n' \"${GITHUB_USERNAME:-x-access-token}\" ;;",
|
|
" *Password*) printf '%s\\n' \"$GITHUB_TOKEN\" ;;",
|
|
" *) printf '\\n' ;;",
|
|
"esac",
|
|
"SH_ASKPASS",
|
|
"chmod 0700 /tmp/hwlab-git-askpass.sh",
|
|
`export GITHUB_USERNAME=${shQuote(transport.username)}`,
|
|
"export GIT_ASKPASS=/tmp/hwlab-git-askpass.sh",
|
|
"export GIT_TERMINAL_PROMPT=0",
|
|
"unset GIT_SSH",
|
|
"unset GIT_SSH_COMMAND",
|
|
"remote=\"https://github.com/${repository}.git\"",
|
|
].join("\n");
|
|
}
|
|
const proxyCommand = useDirect ? "" : `ProxyCommand=node /tmp/hwlab-github-proxy-connect.cjs ${proxyHost} ${proxyPort} %h %p`;
|
|
const privateKeyPath = transport.mode === "ssh" ? `/git-ssh/${transport.privateKeySecretKey}` : "/git-ssh/ssh-privatekey";
|
|
const knownHostsCopy = transport.mode === "ssh" && transport.knownHostsSecretKey !== null
|
|
? [`cp ${shQuote(`/git-ssh/${transport.knownHostsSecretKey}`)} /root/.ssh/known_hosts`, "chmod 0600 /root/.ssh/known_hosts"]
|
|
: [];
|
|
return [
|
|
"mkdir -p /root/.ssh",
|
|
`cp ${shQuote(privateKeyPath)} /root/.ssh/id_rsa`,
|
|
"chmod 0400 /root/.ssh/id_rsa",
|
|
...knownHostsCopy,
|
|
...common,
|
|
...proxyConnectBlock(useDirect),
|
|
"cat > /tmp/hwlab-git-ssh-proxy.sh <<'SH_PROXY'",
|
|
"#!/bin/sh",
|
|
useDirect
|
|
? `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 "$@"`
|
|
: `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 ${shQuote(proxyCommand)} "$@"`,
|
|
"SH_PROXY",
|
|
"chmod 0700 /tmp/hwlab-git-ssh-proxy.sh",
|
|
"export GIT_SSH=/tmp/hwlab-git-ssh-proxy.sh",
|
|
"unset GIT_SSH_COMMAND",
|
|
"remote=\"ssh://git@ssh.github.com:443/${repository}.git\"",
|
|
].filter(Boolean).join("\n");
|
|
}
|
|
|
|
function proxyConnectBlock(useDirect: boolean): string[] {
|
|
if (useDirect) return [];
|
|
return [
|
|
"cat > /tmp/hwlab-github-proxy-connect.cjs <<'NODE_PROXY'",
|
|
readScript("git-mirror-proxy-connect.cjs").trimEnd(),
|
|
"NODE_PROXY",
|
|
"chmod 0700 /tmp/hwlab-github-proxy-connect.cjs",
|
|
];
|
|
}
|
|
|
|
function shQuote(value: string): string {
|
|
return `'${value.replace(/'/gu, `'"'"'`)}'`;
|
|
}
|