Merge pull request #1164 from pikasTech/fix/1148-moonbridge-runtime-image

fix: add YAML runtime image build for D518 HWLAB
This commit is contained in:
Lyon
2026-06-27 22:36:57 +08:00
committed by GitHub
5 changed files with 199 additions and 21 deletions
+24 -6
View File
@@ -427,6 +427,15 @@ lanes:
runtimeImageRewrites:
- source: fatedier/frpc:v0.68.1
target: 127.0.0.1:5000/hwlab/frpc:v0.68.1
runtimeImageBuilds:
- id: moonbridge
kind: moonbridge
target: 127.0.0.1:5000/hwlab/moonbridge:1b99888d3dae
sourceRepo: https://github.com/ZhiYi-R/moon-bridge.git
sourceRef: 1b99888d3dae889b79ee602cb875c7907f7e76f2
builderImage: golang:1.25-bookworm
goProxy: https://goproxy.cn,direct
dockerNetworkMode: host
public:
webUrl: https://hwlab.pikapython.com
apiUrl: https://hwlab.pikapython.com
@@ -628,6 +637,15 @@ lanes:
runtimeImageRewrites:
- source: fatedier/frpc:v0.68.1
target: 127.0.0.1:5000/hwlab/frpc:v0.68.1
runtimeImageBuilds:
- id: moonbridge
kind: moonbridge
target: 127.0.0.1:5000/hwlab/moonbridge:1b99888d3dae
sourceRepo: https://github.com/ZhiYi-R/moon-bridge.git
sourceRef: 1b99888d3dae889b79ee602cb875c7907f7e76f2
builderImage: golang:1.25-bookworm
goProxy: https://goproxy.cn,direct
dockerNetworkMode: host
public:
webUrl: https://hwlab.pikapython.com
apiUrl: https://hwlab.pikapython.com
@@ -795,9 +813,9 @@ networkProfiles:
- 82.156.23.220
- 74.48.78.17
dockerBuildProxy:
http: http://sub2api-egress-proxy.platform-infra.svc.cluster.local:10808
https: http://sub2api-egress-proxy.platform-infra.svc.cluster.local:10808
all: http://sub2api-egress-proxy.platform-infra.svc.cluster.local:10808
http: http://127.0.0.1:10808
https: http://127.0.0.1:10808
all: http://127.0.0.1:10808
noProxy:
- localhost
- 127.0.0.1
@@ -832,9 +850,9 @@ networkProfiles:
- 82.156.23.220
- 74.48.78.17
dockerBuildProxy:
http: http://sub2api-egress-proxy.platform-infra.svc.cluster.local:10808
https: http://sub2api-egress-proxy.platform-infra.svc.cluster.local:10808
all: http://sub2api-egress-proxy.platform-infra.svc.cluster.local:10808
http: http://127.0.0.1:10808
https: http://127.0.0.1:10808
all: http://127.0.0.1:10808
noProxy:
- localhost
- 127.0.0.1
+1 -1
View File
@@ -246,7 +246,7 @@ targets:
image: ghcr.io/sagernet/sing-box:latest
imagePullPolicy: IfNotPresent
listenPort: 10808
hostNetwork: false
hostNetwork: true
sourceConfigRef: config/platform-infra/egress-proxy-sources.yaml#sources.master-shadowsocks
applyToSub2Api: true
applyToSentinel: true
+42
View File
@@ -254,6 +254,17 @@ export interface HwlabRuntimeImageRewriteSpec {
readonly target: string;
}
export interface HwlabRuntimeImageBuildSpec {
readonly id: string;
readonly kind: "moonbridge";
readonly target: string;
readonly sourceRepo: string;
readonly sourceRef: string;
readonly builderImage: string;
readonly goProxy: string;
readonly dockerNetworkMode: "default" | "host";
}
export interface HwlabRuntimePublicExposureFrpcProxySpec {
readonly name: string;
readonly localIP: string;
@@ -346,6 +357,7 @@ export interface HwlabRuntimeLaneSpec {
readonly publicExposure: HwlabRuntimePublicExposureSpec | null;
readonly observability: HwlabRuntimeObservabilitySpec;
readonly runtimeImageRewrites: readonly HwlabRuntimeImageRewriteSpec[];
readonly runtimeImageBuilds: readonly HwlabRuntimeImageBuildSpec[];
readonly networkProfileId: string;
readonly downloadProfileId: string;
readonly networkProfile: HwlabNetworkProfileSpec;
@@ -395,6 +407,7 @@ interface HwlabLaneConfig {
readonly publicExposure: HwlabRuntimePublicExposureSpec | null;
readonly observability: HwlabRuntimeObservabilitySpec;
readonly runtimeImageRewrites: readonly HwlabRuntimeImageRewriteSpec[];
readonly runtimeImageBuilds: readonly HwlabRuntimeImageBuildSpec[];
}
interface HwlabNodeLaneConfig {
@@ -421,6 +434,12 @@ function stringField(obj: Record<string, unknown>, key: string, path: string): s
return value;
}
function enumStringField<T extends string>(obj: Record<string, unknown>, key: string, path: string, allowed: readonly T[]): T {
const value = stringField(obj, key, path);
if (!allowed.includes(value as T)) throw new Error(`${path}.${key} must be one of ${allowed.join(", ")}`);
return value as T;
}
function numberField(obj: Record<string, unknown>, key: string, path: string): number {
const value = obj[key];
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) throw new Error(`${path}.${key} must be a positive integer`);
@@ -630,6 +649,7 @@ function laneConfig(id: HwlabRuntimeLane, raw: Record<string, unknown>): HwlabLa
publicExposure: publicExposureConfig(raw.publicExposure, `lanes.${id}.publicExposure`),
observability: observabilityConfig(raw.observability, `lanes.${id}.observability`),
runtimeImageRewrites: runtimeImageRewritesConfig(raw.runtimeImageRewrites, `lanes.${id}.runtimeImageRewrites`),
runtimeImageBuilds: runtimeImageBuildsConfig(raw.runtimeImageBuilds, `lanes.${id}.runtimeImageBuilds`),
};
}
@@ -1149,6 +1169,27 @@ function runtimeImageRewritesConfig(value: unknown, path: string): HwlabRuntimeI
});
}
function runtimeImageBuildsConfig(value: unknown, path: string): HwlabRuntimeImageBuildSpec[] {
if (value === undefined) return [];
if (!Array.isArray(value)) throw new Error(`${path} must be an array`);
return value.map((item, index) => {
const itemPath = `${path}[${index}]`;
const raw = asRecord(item, itemPath);
const kind = stringField(raw, "kind", itemPath);
if (kind !== "moonbridge") throw new Error(`${itemPath}.kind must be moonbridge`);
return {
id: stringField(raw, "id", itemPath),
kind,
target: stringField(raw, "target", itemPath),
sourceRepo: stringField(raw, "sourceRepo", itemPath),
sourceRef: stringField(raw, "sourceRef", itemPath),
builderImage: stringField(raw, "builderImage", itemPath),
goProxy: stringField(raw, "goProxy", itemPath),
dockerNetworkMode: enumStringField(raw, "dockerNetworkMode", itemPath, ["default", "host"]),
};
});
}
function readHwlabNodeLaneConfig(): HwlabNodeLaneConfig {
const path = rootPath(HWLAB_NODE_LANE_CONFIG_PATH);
const raw = readFileSync(path, "utf8");
@@ -1273,6 +1314,7 @@ function buildRuntimeLaneSpec(config: HwlabLaneConfig): HwlabRuntimeLaneSpec {
publicExposure: config.publicExposure,
observability: config.observability,
runtimeImageRewrites: config.runtimeImageRewrites,
runtimeImageBuilds: config.runtimeImageBuilds,
networkProfileId: networkProfile.id,
downloadProfileId: downloadProfile.id,
networkProfile,
+1
View File
@@ -153,6 +153,7 @@ export function nodeRuntimeExpected(spec: HwlabRuntimeLaneSpec): Record<string,
},
observability: spec.observability,
runtimeImageRewrites: spec.runtimeImageRewrites,
runtimeImageBuilds: spec.runtimeImageBuilds,
externalPostgres: spec.externalPostgres === undefined ? null : {
active: activeExternalPostgres !== undefined,
provider: spec.externalPostgres.provider,
+131 -14
View File
@@ -33,7 +33,7 @@ import { isCommandSuccess, nodeRuntimeGitopsRoot, runNodeK3sArgs, runNodeK3sScri
import { nodeRuntimeCicdWaitWarning } from "./git-mirror";
import { parseNodeScopedDelegatedOptions } from "./plan";
import { nodeRuntimePipelineFailureSummary, nodeRuntimePipelineRunDiagnostics } from "./render";
import { compactRuntimeCommand, runNodeHostScript } from "./runtime-common";
import { compactRuntimeCommand, runNodeHostScript, runNodeHostScriptAsync } from "./runtime-common";
import { assertLane, assertNodeId, keyValueLinesFromText, numericField, optionValue, optionalStringValue, positiveIntegerOption, positiveIntegerValue, record, requiredOption, shellQuote, statusText, stringValue, stripOptions } from "./utils";
import { discoverWebObserveIndexEntry, readWebObserveIndexEntry } from "./web-observe-render";
import { assertKnownOptions, nodeWebProbeAutoCommandTimeoutSeconds, nodeWebProbeDefaultUrl, normalizeNodeWebProbeObserveArgs, parseNodeWebProbeObserveOptions, parseNodeWebProbeSentinelOptions, parseWebProbeBrowserProxyMode } from "./web-probe-observe";
@@ -95,6 +95,7 @@ export function nodeRuntimeRenderOverlay(spec: HwlabRuntimeLaneSpec): Record<str
observability: spec.observability,
runtimeStore: spec.runtimeStore,
runtimeImageRewrites: spec.runtimeImageRewrites,
runtimeImageBuilds: spec.runtimeImageBuilds,
registryPrefix: spec.registryPrefix,
buildkitSidecarImage: spec.buildkit?.sidecarImage,
publicWebUrl: spec.publicWebUrl,
@@ -809,24 +810,54 @@ interface NodeRuntimeImageDependency {
id: string;
target: string;
source: string;
mode: "pull" | "build-moonbridge";
sourceRepo?: string;
sourceRef?: string;
builderImage?: string;
goProxy?: string;
dockerNetworkMode?: "default" | "host";
}
function nodeRuntimeImageDependencies(spec: HwlabRuntimeLaneSpec): NodeRuntimeImageDependency[] {
const images: NodeRuntimeImageDependency[] = [
{ id: "base", target: spec.baseImage, source: spec.baseImageSource ?? "" },
{ id: "base", target: spec.baseImage, source: spec.baseImageSource ?? "", mode: "pull" },
];
if (spec.buildkit !== undefined) {
images.push({ id: "buildkit", target: spec.buildkit.sidecarImage, source: spec.buildkit.sourceImage });
images.push({ id: "buildkit", target: spec.buildkit.sidecarImage, source: spec.buildkit.sourceImage, mode: "pull" });
}
spec.runtimeImageRewrites.forEach((rewrite, index) => {
images.push({ id: `rewrite-${index + 1}`, target: rewrite.target, source: rewrite.source });
images.push({ id: `rewrite-${index + 1}`, target: rewrite.target, source: rewrite.source, mode: "pull" });
});
spec.runtimeImageBuilds.forEach((build) => {
images.push({
id: build.id,
target: build.target,
source: `${build.sourceRepo}#${build.sourceRef}`,
mode: "build-moonbridge",
sourceRepo: build.sourceRepo,
sourceRef: build.sourceRef,
builderImage: build.builderImage,
goProxy: build.goProxy,
dockerNetworkMode: build.dockerNetworkMode,
});
});
return images;
}
function nodeRuntimeImageCalls(spec: HwlabRuntimeLaneSpec, functionName: string): string {
return nodeRuntimeImageDependencies(spec)
.map((image) => `${functionName} ${shellQuote(image.id)} ${shellQuote(image.target)} ${shellQuote(image.source)}`)
.map((image) => [
functionName,
image.id,
image.target,
image.source,
image.mode,
image.sourceRepo ?? "",
image.sourceRef ?? "",
image.builderImage ?? "",
image.goProxy ?? "",
image.dockerNetworkMode ?? "",
].map(shellQuote).join(" "))
.join("\n");
}
@@ -845,7 +876,7 @@ export function nodeRuntimeBaseImageStatus(spec: HwlabRuntimeLaneSpec, timeoutSe
const script = [
"set -eu",
"check_image() {",
" id=\"$1\"; target=\"$2\"; source=\"$3\"",
" id=\"$1\"; target=\"$2\"; source=\"$3\"; mode=\"$4\"",
" repo_tag=${target#*/}",
" repo=${repo_tag%:*}",
" tag=${repo_tag##*:}",
@@ -856,12 +887,12 @@ export function nodeRuntimeBaseImageStatus(spec: HwlabRuntimeLaneSpec, timeoutSe
" target_present=false",
" if docker image inspect \"$target\" >/dev/null 2>&1; then target_present=true; fi",
" source_present=false",
" if [ -n \"$source\" ] && docker image inspect \"$source\" >/dev/null 2>&1; then source_present=true; fi",
" if [ \"$mode\" = build-moonbridge ]; then source_present=true; elif [ -n \"$source\" ] && docker image inspect \"$source\" >/dev/null 2>&1; then source_present=true; fi",
" source_configured=false",
" if [ -n \"$source\" ]; then source_configured=true; fi",
" python3 - \"$id\" \"$target\" \"$source\" \"$source_configured\" \"$registry_url\" \"$registry_present\" \"$target_present\" \"$source_present\" <<'PY'",
" python3 - \"$id\" \"$target\" \"$source\" \"$mode\" \"$source_configured\" \"$registry_url\" \"$registry_present\" \"$target_present\" \"$source_present\" <<'PY'",
"import json, sys",
"keys=['id','target','source','sourceConfigured','registryUrl','registryTagPresent','targetImagePresent','sourceImagePresent']",
"keys=['id','target','source','mode','sourceConfigured','registryUrl','registryTagPresent','targetImagePresent','sourceImagePresent']",
"data=dict(zip(keys, sys.argv[1:]))",
"for key in ['sourceConfigured','registryTagPresent','targetImagePresent','sourceImagePresent']:",
" data[key] = data[key] == 'true'",
@@ -893,9 +924,57 @@ export function ensureNodeBaseImage(spec: HwlabRuntimeLaneSpec, dryRun: boolean,
"set -eu",
`dry_run=${shellQuote(dryRun ? "true" : "false")}`,
`pull_retries=${shellQuote(String(Math.max(1, spec.downloadProfile.docker.pullRetries)))}`,
`build_http_proxy=${shellQuote(spec.networkProfile.dockerBuildProxy.http)}`,
`build_https_proxy=${shellQuote(spec.networkProfile.dockerBuildProxy.https)}`,
`build_all_proxy=${shellQuote(spec.networkProfile.dockerBuildProxy.all)}`,
`build_no_proxy=${shellQuote(spec.networkProfile.dockerBuildProxy.noProxy.join(","))}`,
"resolve_host_proxy_url() {",
" raw_url=\"$1\"",
" if [ -z \"$raw_url\" ]; then printf '\\n'; return 0; fi",
" info=$(python3 - \"$raw_url\" <<'PY'",
"from urllib.parse import urlparse",
"import sys",
"url = sys.argv[1]",
"parsed = urlparse(url)",
"host = parsed.hostname or ''",
"port = parsed.port",
"scheme = parsed.scheme or 'http'",
"parts = host.split('.')",
"if len(parts) >= 4 and parts[2] == 'svc':",
" print('\\t'.join(['service', scheme, parts[0], parts[1], str(port or 80)]))",
"else:",
" print('\\t'.join(['literal', url]))",
"PY",
" )",
" kind=$(printf '%s' \"$info\" | cut -f1)",
" service_cluster_ip() {",
" service_namespace=\"$1\"; service_name=\"$2\"",
" cluster_ip=\"\"",
" if command -v kubectl >/dev/null 2>&1; then",
" cluster_ip=$(kubectl -n \"$service_namespace\" get svc \"$service_name\" -o jsonpath='{.spec.clusterIP}' 2>/dev/null || true)",
" fi",
" if { [ -z \"$cluster_ip\" ] || [ \"$cluster_ip\" = None ]; } && command -v k3s >/dev/null 2>&1; then",
" cluster_ip=$(k3s kubectl -n \"$service_namespace\" get svc \"$service_name\" -o jsonpath='{.spec.clusterIP}' 2>/dev/null || true)",
" fi",
" printf '%s\\n' \"$cluster_ip\"",
" }",
" if [ \"$kind\" = service ]; then",
" scheme=$(printf '%s' \"$info\" | cut -f2)",
" service_name=$(printf '%s' \"$info\" | cut -f3)",
" service_namespace=$(printf '%s' \"$info\" | cut -f4)",
" service_port=$(printf '%s' \"$info\" | cut -f5)",
" cluster_ip=$(service_cluster_ip \"$service_namespace\" \"$service_name\")",
" if [ -n \"$cluster_ip\" ] && [ \"$cluster_ip\" != None ]; then printf '%s://%s:%s\\n' \"$scheme\" \"$cluster_ip\" \"$service_port\"; else printf '%s\\n' \"$raw_url\"; fi",
" else",
" printf '%s\\n' \"$raw_url\"",
" fi",
"}",
"build_http_proxy=$(resolve_host_proxy_url \"$build_http_proxy\")",
"build_https_proxy=$(resolve_host_proxy_url \"$build_https_proxy\")",
"build_all_proxy=$(resolve_host_proxy_url \"$build_all_proxy\")",
"failed=false",
"ensure_image() {",
" id=\"$1\"; target=\"$2\"; source=\"$3\"",
" id=\"$1\"; target=\"$2\"; source=\"$3\"; mode=\"$4\"; source_repo=\"$5\"; source_ref=\"$6\"; builder_image=\"$7\"; go_proxy=\"$8\"; docker_network_mode=\"$9\"",
" repo_tag=${target#*/}",
" repo=${repo_tag%:*}",
" tag=${repo_tag##*:}",
@@ -910,13 +989,47 @@ export function ensureNodeBaseImage(spec: HwlabRuntimeLaneSpec, dryRun: boolean,
" if [ \"$present\" = false ]; then",
" action=seed",
" if [ \"$dry_run\" = false ]; then",
" if [ \"$mode\" = build-moonbridge ]; then",
" action=build",
" source_present_before=build-source",
" tmpdir=$(mktemp -d /tmp/hwlab-node-runtime-image-$id.XXXXXX)",
" dockerfile=\"$tmpdir/Dockerfile\"",
" cat > \"$dockerfile\" <<'DOCKERFILE'",
"ARG BUILDER_IMAGE",
"FROM ${BUILDER_IMAGE} AS builder",
"ARG MOONBRIDGE_REPO",
"ARG MOONBRIDGE_REF",
"ARG GOPROXY_VALUE",
"ENV GOPROXY=${GOPROXY_VALUE}",
"WORKDIR /src",
"RUN git clone --filter=blob:none \"${MOONBRIDGE_REPO}\" moon-bridge",
"WORKDIR /src/moon-bridge",
"RUN git checkout \"${MOONBRIDGE_REF}\" \\",
" && go mod download \\",
" && CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags=\"-s -w\" -o /out/moonbridge ./cmd/moonbridge",
"FROM scratch",
"WORKDIR /app",
"COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt",
"COPY --from=builder /out/moonbridge /app/moonbridge",
"EXPOSE 4001",
"USER 65532:65532",
"ENTRYPOINT [\"/app/moonbridge\"]",
"DOCKERFILE",
" if env HTTP_PROXY=\"$build_http_proxy\" HTTPS_PROXY=\"$build_https_proxy\" ALL_PROXY=\"$build_all_proxy\" NO_PROXY=\"$build_no_proxy\" http_proxy=\"$build_http_proxy\" https_proxy=\"$build_https_proxy\" all_proxy=\"$build_all_proxy\" no_proxy=\"$build_no_proxy\" docker build --network \"$docker_network_mode\" --build-arg BUILDER_IMAGE=\"$builder_image\" --build-arg MOONBRIDGE_REPO=\"$source_repo\" --build-arg MOONBRIDGE_REF=\"$source_ref\" --build-arg GOPROXY_VALUE=\"$go_proxy\" --build-arg HTTP_PROXY=\"$build_http_proxy\" --build-arg HTTPS_PROXY=\"$build_https_proxy\" --build-arg ALL_PROXY=\"$build_all_proxy\" --build-arg NO_PROXY=\"$build_no_proxy\" -t \"$target\" -f \"$dockerfile\" \"$tmpdir\" >/tmp/hwlab-node-runtime-image-$id-build.out 2>&1; then",
" docker push \"$target\" >/tmp/hwlab-node-runtime-image-$id-push.out 2>&1 || { cat /tmp/hwlab-node-runtime-image-$id-push.out >&2 2>/dev/null || true; failed=true; }",
" else",
" cat /tmp/hwlab-node-runtime-image-$id-build.out >&2 2>/dev/null || true",
" failed=true",
" fi",
" rm -rf \"$tmpdir\"",
" else",
" source_present_before=true",
" if ! docker image inspect \"$source\" >/dev/null 2>&1; then",
" source_present_before=false",
" attempt=1",
" while [ \"$attempt\" -le \"$pull_retries\" ]; do",
" pull_attempts=$attempt",
" if docker pull \"$source\" >/tmp/hwlab-node-runtime-image-$id-pull.out 2>&1; then pulled_source=true; break; fi",
" if env HTTP_PROXY=\"$build_http_proxy\" HTTPS_PROXY=\"$build_https_proxy\" ALL_PROXY=\"$build_all_proxy\" NO_PROXY=\"$build_no_proxy\" http_proxy=\"$build_http_proxy\" https_proxy=\"$build_https_proxy\" all_proxy=\"$build_all_proxy\" no_proxy=\"$build_no_proxy\" docker pull \"$source\" >/tmp/hwlab-node-runtime-image-$id-pull.out 2>&1; then pulled_source=true; break; fi",
" attempt=$((attempt + 1))",
" sleep 2",
" done",
@@ -930,13 +1043,14 @@ export function ensureNodeBaseImage(spec: HwlabRuntimeLaneSpec, dryRun: boolean,
" failed=true",
" fi",
" fi",
" fi",
" fi",
" fi",
" after=false",
" if curl -fsS \"$registry_url\" 2>/dev/null | grep -q '\"'\"$tag\"'\"'; then after=true; fi",
" python3 - \"$id\" \"$target\" \"$source\" \"$dry_run\" \"$present\" \"$after\" \"$action\" \"$pull_retries\" \"$source_present_before\" \"$pulled_source\" \"$pull_attempts\" <<'PY'",
" python3 - \"$id\" \"$target\" \"$source\" \"$mode\" \"$dry_run\" \"$present\" \"$after\" \"$action\" \"$pull_retries\" \"$source_present_before\" \"$pulled_source\" \"$pull_attempts\" <<'PY'",
"import json, sys",
"keys=['id','target','source','dryRun','presentBefore','presentAfter','action','pullRetries','sourcePresentBefore','pulledSource','pullAttempts']",
"keys=['id','target','source','mode','dryRun','presentBefore','presentAfter','action','pullRetries','sourcePresentBefore','pulledSource','pullAttempts']",
"data=dict(zip(keys, sys.argv[1:]))",
"for key in ['dryRun','presentBefore','presentAfter','pulledSource']:",
" data[key] = data[key] == 'true'",
@@ -949,7 +1063,10 @@ export function ensureNodeBaseImage(spec: HwlabRuntimeLaneSpec, dryRun: boolean,
nodeRuntimeImageCalls(spec, "ensure_image"),
"if [ \"$failed\" = true ]; then exit 1; fi",
].join("\n");
const result = runNodeHostScript(spec, script, Math.min(timeoutSeconds, 300));
const hasBuild = nodeRuntimeImageDependencies(spec).some((image) => image.mode === "build-moonbridge");
const result = hasBuild && !dryRun
? runNodeHostScriptAsync(spec, script, Math.min(timeoutSeconds, 600), "runtime-image-build")
: runNodeHostScript(spec, script, Math.min(timeoutSeconds, 300));
const imageRows = parseNodeRuntimeImageRows(statusText(result));
const base = imageRows.find((image) => image.id === "base") ?? {};
return {