fix(ci): reuse provider egress for backend-core artifacts
This commit is contained in:
+152
-9
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { runCommand } from "./command";
|
||||
import { type UniDeskConfig, repoRoot, rootPath } from "./config";
|
||||
import { ensureGithubSshIdentityForProvider, gitSshHttpConnectProxySource } from "./deploy-ssh-identity";
|
||||
import { startJob } from "./jobs";
|
||||
import { coreInternalFetch } from "./microservices";
|
||||
|
||||
@@ -38,6 +39,7 @@ interface CiPublishBackendCoreOptions {
|
||||
repoUrl: string;
|
||||
commit: string;
|
||||
waitMs: number;
|
||||
sourceHostPath: string;
|
||||
}
|
||||
|
||||
interface CiDevE2EOptions {
|
||||
@@ -64,6 +66,20 @@ interface DispatchResult {
|
||||
raw: unknown;
|
||||
}
|
||||
|
||||
interface PipelineRunCondition {
|
||||
ok: boolean | null;
|
||||
status: string;
|
||||
reason: string;
|
||||
message: string;
|
||||
query: {
|
||||
ok: boolean;
|
||||
status: string | null;
|
||||
exitCode: number | null;
|
||||
stdoutTail: string;
|
||||
stderrTail: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeployDevManifestSummary {
|
||||
deployCommit: string;
|
||||
desiredRef: string;
|
||||
@@ -124,6 +140,15 @@ function shellQuote(value: string): string {
|
||||
return `'${value.replace(/'/gu, "'\\''")}'`;
|
||||
}
|
||||
|
||||
function repoSshUrl(repoUrl: string): string {
|
||||
if (repoUrl.startsWith("git@")) return repoUrl;
|
||||
if (repoUrl.startsWith("https://github.com/")) {
|
||||
const repoPath = repoUrl.slice("https://github.com/".length).replace(/\.git$/u, "");
|
||||
return `git@github.com:${repoPath}.git`;
|
||||
}
|
||||
return repoUrl;
|
||||
}
|
||||
|
||||
function chunks(value: string, size: number): string[] {
|
||||
const result: string[] = [];
|
||||
for (let index = 0; index < value.length; index += size) {
|
||||
@@ -508,6 +533,8 @@ spec:
|
||||
value: ${JSON.stringify(options.repoUrl)}
|
||||
- name: revision
|
||||
value: ${JSON.stringify(options.commit)}
|
||||
- name: source-host-path
|
||||
value: ${JSON.stringify(options.sourceHostPath)}
|
||||
workspaces:
|
||||
- name: shared-workspace
|
||||
persistentVolumeClaim:
|
||||
@@ -515,6 +542,77 @@ spec:
|
||||
`;
|
||||
}
|
||||
|
||||
function backendCoreArtifactSourceHostPath(commit: string): string {
|
||||
return `/home/ubuntu/.unidesk/ci/backend-core-artifacts/${commit}`;
|
||||
}
|
||||
|
||||
async function prepareBackendCoreArtifactSource(config: UniDeskConfig, options: CiPublishBackendCoreOptions): Promise<Record<string, unknown>> {
|
||||
const sshIdentity = await ensureGithubSshIdentityForProvider(config, d601ProviderId);
|
||||
if (!sshIdentity.ok) throw new Error(sshIdentity.detail);
|
||||
const proxyPython = gitSshHttpConnectProxySource();
|
||||
const sourceRoot = "/home/ubuntu/.unidesk/ci/backend-core-artifacts";
|
||||
const sourceHostPath = options.sourceHostPath;
|
||||
const repoCache = "/home/ubuntu/.unidesk/ci/git/unidesk.git";
|
||||
const repoFetchUrl = repoSshUrl(options.repoUrl);
|
||||
const script = [
|
||||
"set -euo pipefail",
|
||||
`commit=${shellQuote(options.commit)}`,
|
||||
`repo_url=${shellQuote(options.repoUrl)}`,
|
||||
`repo_fetch_url=${shellQuote(repoFetchUrl)}`,
|
||||
`source_root=${shellQuote(sourceRoot)}`,
|
||||
`source_dir=${shellQuote(sourceHostPath)}`,
|
||||
`repo_cache=${shellQuote(repoCache)}`,
|
||||
`proxy_url=${shellQuote(providerGatewayWsEgressProxyUrl)}`,
|
||||
"mkdir -p \"$(dirname \"$repo_cache\")\" \"$source_root\"",
|
||||
"export HTTP_PROXY=\"$proxy_url\" HTTPS_PROXY=\"$proxy_url\" ALL_PROXY=\"$proxy_url\"",
|
||||
"export NO_PROXY=\"localhost,127.0.0.1,::1,host.docker.internal,.svc,.cluster.local,kubernetes.default.svc\"",
|
||||
"curl -fsSI --max-time 20 -x \"$proxy_url\" https://github.com >/dev/null",
|
||||
"git_ssh_proxy=/tmp/unidesk-git-ssh-http-connect.py",
|
||||
"cat > \"$git_ssh_proxy\" <<'UNIDESK_GIT_SSH_PROXY'",
|
||||
proxyPython,
|
||||
"UNIDESK_GIT_SSH_PROXY",
|
||||
"chmod 700 \"$git_ssh_proxy\"",
|
||||
"export UNIDESK_GIT_SSH_HTTP_PROXY=\"$proxy_url\"",
|
||||
"export GIT_SSH_COMMAND=\"ssh -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$HOME/.ssh/known_hosts -i $HOME/.ssh/id_ed25519 -o 'ProxyCommand=$git_ssh_proxy %h %p'\"",
|
||||
"echo backend_core_artifact_source_proxy=provider-gateway-ws-egress:$proxy_url",
|
||||
"echo backend_core_artifact_repo_fetch_url=$repo_fetch_url",
|
||||
"if [ ! -d \"$repo_cache\" ]; then git clone --mirror \"$repo_fetch_url\" \"$repo_cache\"; fi",
|
||||
"git -C \"$repo_cache\" remote set-url origin \"$repo_fetch_url\"",
|
||||
"git -C \"$repo_cache\" fetch --no-tags origin \"$commit\" || git -C \"$repo_cache\" fetch --no-tags origin '+refs/heads/*:refs/remotes/origin/*'",
|
||||
"resolved=$(git -C \"$repo_cache\" rev-parse --verify \"$commit^{commit}\")",
|
||||
"test \"$resolved\" = \"$commit\" || { echo \"backend_core_artifact_resolved_commit_mismatch=$resolved expected=$commit\" >&2; exit 1; }",
|
||||
"git -C \"$repo_cache\" cat-file -e \"$commit:src/components/backend-core/Dockerfile\"",
|
||||
"git -C \"$repo_cache\" cat-file -e \"$commit:src/components/backend-core/src\"",
|
||||
"tmp_dir=\"$source_root/.tmp-$commit-$$\"",
|
||||
"rm -rf \"$tmp_dir\"",
|
||||
"mkdir -p \"$tmp_dir\"",
|
||||
"git -C \"$repo_cache\" archive \"$commit\" | tar -x -C \"$tmp_dir\"",
|
||||
"printf '%s\\n' \"$commit\" > \"$tmp_dir/.unidesk-source-commit\"",
|
||||
"printf '%s\\n' \"$repo_url\" > \"$tmp_dir/.unidesk-source-repo\"",
|
||||
"rm -rf \"$source_dir\"",
|
||||
"mv \"$tmp_dir\" \"$source_dir\"",
|
||||
"test -f \"$source_dir/src/components/backend-core/Dockerfile\"",
|
||||
"test -d \"$source_dir/src/components/backend-core/src\"",
|
||||
"echo backend_core_artifact_source_host_path=$source_dir",
|
||||
].join("\n");
|
||||
const result = await runRemoteBackground("prepare-backend-core-source", script, 300_000);
|
||||
if (!result.ok) throw new Error(`failed to prepare backend-core source on D601: ${result.stderr || result.stdout || JSON.stringify(result.raw)}`);
|
||||
return {
|
||||
ok: true,
|
||||
mode: "d601-host-github-ssh-export",
|
||||
providerId: d601ProviderId,
|
||||
repoUrl: options.repoUrl,
|
||||
repoFetchUrl,
|
||||
commit: options.commit,
|
||||
sourceHostPath,
|
||||
identity: {
|
||||
fingerprint: sshIdentity.fingerprint,
|
||||
seededFromLocal: sshIdentity.seededFromLocal,
|
||||
},
|
||||
stdoutTail: result.stdout.slice(-4000),
|
||||
};
|
||||
}
|
||||
|
||||
async function remoteCreatePipelineRun(manifest: string): Promise<string> {
|
||||
const encoded = Buffer.from(manifest, "utf8").toString("base64");
|
||||
const token = randomUUID().replace(/-/gu, "").slice(0, 12);
|
||||
@@ -536,18 +634,19 @@ async function waitForPipelineRun(name: string, waitMs: number): Promise<Dispatc
|
||||
const command = [
|
||||
"set -euo pipefail",
|
||||
`export KUBECONFIG=${shellQuote(d601Kubeconfig)}`,
|
||||
`printf 'waiting_pipelinerun=%s\\n' ${shellQuote(name)}`,
|
||||
`deadline=$((SECONDS + ${Math.ceil(waitMs / 1000)}))`,
|
||||
"while [ \"$SECONDS\" -lt \"$deadline\" ]; do",
|
||||
` condition="$(kubectl get pipelinerun/${shellQuote(name)} -n unidesk-ci -o jsonpath='{range .status.conditions[?(@.type==\"Succeeded\")]}{.status}{\"\\t\"}{.reason}{\"\\t\"}{.message}{end}' 2>/dev/null || true)"`,
|
||||
" case \"$condition\" in",
|
||||
" True*)",
|
||||
" echo \"$condition\"",
|
||||
` kubectl get pipelinerun/${shellQuote(name)} -n unidesk-ci -o json`,
|
||||
" printf 'condition=%s\\n' \"$condition\"",
|
||||
` kubectl get taskrun -n unidesk-ci -l tekton.dev/pipelineRun=${shellQuote(name)} --no-headers 2>/dev/null || true`,
|
||||
" exit 0",
|
||||
" ;;",
|
||||
" False*)",
|
||||
" echo \"$condition\"",
|
||||
` kubectl get pipelinerun/${shellQuote(name)} -n unidesk-ci -o json`,
|
||||
" printf 'condition=%s\\n' \"$condition\"",
|
||||
` kubectl get taskrun,pod -n unidesk-ci -l tekton.dev/pipelineRun=${shellQuote(name)} 2>/dev/null || true`,
|
||||
" exit 1",
|
||||
" ;;",
|
||||
" esac",
|
||||
@@ -560,10 +659,41 @@ async function waitForPipelineRun(name: string, waitMs: number): Promise<Dispatc
|
||||
return dispatchSsh(command, waitMs + 30_000, waitMs + 20_000);
|
||||
}
|
||||
|
||||
async function readPipelineRunCondition(name: string): Promise<PipelineRunCondition> {
|
||||
const result = await runRemoteKubectlRaw([
|
||||
"set -euo pipefail",
|
||||
`condition="$(kubectl get pipelinerun/${shellQuote(name)} -n unidesk-ci -o jsonpath='{range .status.conditions[?(@.type==\"Succeeded\")]}{.status}{\"\\t\"}{.reason}{\"\\t\"}{.message}{end}' 2>/dev/null || true)"`,
|
||||
"printf '%s\\n' \"$condition\"",
|
||||
].join("\n"), 30_000, 15_000);
|
||||
const [status = "", reason = "", ...messageParts] = result.stdout.trim().split("\t");
|
||||
const message = messageParts.join("\t");
|
||||
return {
|
||||
ok: status === "True" ? true : status === "False" ? false : null,
|
||||
status,
|
||||
reason,
|
||||
message,
|
||||
query: {
|
||||
ok: result.ok,
|
||||
status: result.status,
|
||||
exitCode: result.exitCode,
|
||||
stdoutTail: result.stdout.slice(-1200),
|
||||
stderrTail: result.stderr.slice(-1200),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function pipelineRunWaitSucceeded(wait: DispatchResult | null, condition: PipelineRunCondition | null): boolean {
|
||||
if (wait === null) return true;
|
||||
if (condition?.ok === true) return true;
|
||||
if (condition?.ok === false) return false;
|
||||
return wait.ok || wait.exitCode === 0 || wait.stdout.includes("condition=True\tSucceeded\t");
|
||||
}
|
||||
|
||||
async function run(options: CiOptions): Promise<Record<string, unknown>> {
|
||||
const name = await remoteCreatePipelineRun(pipelineRunManifest(options));
|
||||
const wait = await waitForPipelineRun(name, options.waitMs);
|
||||
const waitSucceeded = wait === null || wait.exitCode === 0 || wait.stdout.trimStart().startsWith("True\tSucceeded\t");
|
||||
const condition = wait === null ? null : await readPipelineRunCondition(name);
|
||||
const waitSucceeded = pipelineRunWaitSucceeded(wait, condition);
|
||||
return {
|
||||
ok: waitSucceeded,
|
||||
pipelineRun: name,
|
||||
@@ -571,9 +701,14 @@ async function run(options: CiOptions): Promise<Record<string, unknown>> {
|
||||
repoUrl: options.repoUrl,
|
||||
revision: options.revision,
|
||||
wait: wait === null ? null : {
|
||||
ok: waitSucceeded,
|
||||
dispatchOk: wait.ok,
|
||||
dispatchStatus: wait.status,
|
||||
dispatchExitCode: wait.exitCode,
|
||||
stdoutTail: wait.stdout.slice(-6000),
|
||||
stderrTail: wait.stderr.slice(-6000),
|
||||
},
|
||||
condition,
|
||||
next: [
|
||||
`bun scripts/cli.ts ci logs ${name}`,
|
||||
"bun scripts/cli.ts ci status",
|
||||
@@ -581,22 +716,30 @@ async function run(options: CiOptions): Promise<Record<string, unknown>> {
|
||||
};
|
||||
}
|
||||
|
||||
async function publishBackendCoreArtifact(options: CiPublishBackendCoreOptions): Promise<Record<string, unknown>> {
|
||||
async function publishBackendCoreArtifact(config: UniDeskConfig, options: CiPublishBackendCoreOptions): Promise<Record<string, unknown>> {
|
||||
const source = await prepareBackendCoreArtifactSource(config, options);
|
||||
const name = await remoteCreatePipelineRun(backendCoreArtifactPipelineRunManifest(options));
|
||||
const wait = await waitForPipelineRun(name, options.waitMs);
|
||||
const waitSucceeded = wait === null || wait.exitCode === 0 || wait.stdout.trimStart().startsWith("True\tSucceeded\t");
|
||||
const condition = wait === null ? null : await readPipelineRunCondition(name);
|
||||
const waitSucceeded = pipelineRunWaitSucceeded(wait, condition);
|
||||
return {
|
||||
ok: waitSucceeded,
|
||||
pipelineRun: name,
|
||||
namespace: "unidesk-ci",
|
||||
repoUrl: options.repoUrl,
|
||||
commit: options.commit,
|
||||
source,
|
||||
artifact: `127.0.0.1:5000/unidesk/backend-core:${options.commit}`,
|
||||
boundary: "CI publishes the image to D601 registry; CD must pull it and must not build backend-core",
|
||||
wait: wait === null ? null : {
|
||||
ok: waitSucceeded,
|
||||
dispatchOk: wait.ok,
|
||||
dispatchStatus: wait.status,
|
||||
dispatchExitCode: wait.exitCode,
|
||||
stdoutTail: wait.stdout.slice(-6000),
|
||||
stderrTail: wait.stderr.slice(-6000),
|
||||
},
|
||||
condition,
|
||||
next: [
|
||||
`bun scripts/cli.ts ci logs ${name}`,
|
||||
`bun scripts/cli.ts artifact-registry deploy-backend-core --commit ${options.commit}`,
|
||||
@@ -890,7 +1033,7 @@ function requireRunId(value: string): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
export async function runCiCommand(_config: UniDeskConfig, args: string[]): Promise<Record<string, unknown>> {
|
||||
export async function runCiCommand(config: UniDeskConfig, args: string[]): Promise<Record<string, unknown>> {
|
||||
const [action = "status", nameArg] = args;
|
||||
if (isHelpArg(action) || args.slice(1).some(isHelpArg)) return ciHelp();
|
||||
if (action === "install") return install();
|
||||
@@ -905,7 +1048,7 @@ export async function runCiCommand(_config: UniDeskConfig, args: string[]): Prom
|
||||
const repoUrl = stringOption(args, "--repo") ?? stringOption(args, "--repo-url") ?? "https://github.com/pikasTech/unidesk";
|
||||
const commit = requireFullCommit(stringOption(args, "--commit") ?? stringOption(args, "--revision"));
|
||||
const waitMs = numberOption(args, "--wait-ms", 0);
|
||||
return publishBackendCoreArtifact({ repoUrl, commit, waitMs });
|
||||
return publishBackendCoreArtifact(config, { repoUrl, commit, waitMs, sourceHostPath: backendCoreArtifactSourceHostPath(commit) });
|
||||
}
|
||||
if (action === "run-dev-e2e") {
|
||||
const repoUrl = stringOption(args, "--repo") ?? stringOption(args, "--repo-url") ?? "https://github.com/pikasTech/unidesk";
|
||||
|
||||
Reference in New Issue
Block a user