fix: keep temporary Node binaries out of image layers

This commit is contained in:
whyour
2026-10-23 22:02:07 +08:00
parent 7c7d8ab06b
commit b12d2ea9cb
4 changed files with 75 additions and 8 deletions
+23
View File
@@ -9,6 +9,7 @@ on:
- 'docker/Dockerfile*'
- 'shell/node-runtime-probe.cjs'
- 'scripts/verify-panel-package.cjs'
- 'scripts/verify-image-node-layers.py'
- 'test/back/node-runtime-probe.test.cjs'
workflow_dispatch:
@@ -132,6 +133,28 @@ jobs:
docker run --rm --network none --entrypoint node \
qinglong-readiness:${{ matrix.image }} \
-e 'const assert=require("node:assert/strict"); const {createRequire}=require("node:module"); for(const [name,want] of Object.entries({"@grpc/grpc-js":"1.14.5",compression:"1.8.2",multer:"2.4.0",sequelize:"6.37.8"}))assert.equal(require(name+"/package.json").version,want); const expressRequire=createRequire(require.resolve("express")); assert.equal(expressRequire("proxy-addr/package.json").version,"2.0.8"); const sockRequire=createRequire(require.resolve("sockjs")); assert.equal(sockRequire("websocket-driver/package.json").version,"0.7.5"); require("sqlite3"); require("nodemailer"); require("undici"); console.log("Minimal production patches and runtime imports verified")'
- name: Export ARMv6 image layers for Node regression checks
if: startsWith(matrix.image, 'alpine')
uses: docker/build-push-action@v7
with:
context: .
file: ${{ matrix.file }}
platforms: linux/arm/v6
build-args: |
QL_URL=${{ steps.source.outputs.url }}
QL_BRANCH=${{ steps.source.outputs.branch }}
SOURCE_COMMIT=${{ steps.source.outputs.sha }}
outputs: type=docker,dest=${{ runner.temp }}/node-layers-arm.tar
push: false
provenance: false
sbom: false
- name: Check Node binaries in every serialized image layer
if: startsWith(matrix.image, 'alpine')
run: |
docker save qinglong-readiness:${{ matrix.image }} -o "$RUNNER_TEMP/node-layers-amd64.tar"
python3 scripts/verify-image-node-layers.py "$RUNNER_TEMP/node-layers-amd64.tar" amd64
python3 scripts/verify-image-node-layers.py "$RUNNER_TEMP/node-layers-arm.tar" arm
rm "$RUNNER_TEMP/node-layers-amd64.tar" "$RUNNER_TEMP/node-layers-arm.tar"
- name: Export native image for isolated dev validation
if: matrix.image == 'debian' || matrix.image == 'alpine'
run: |
+4 -4
View File
@@ -29,7 +29,6 @@ FROM python:3.11-alpine
ARG TARGETARCH
ARG TARGETVARIANT
COPY --from=node-runtime-amd64 /usr/local/bin/node /tmp/node-runtime-amd64
ARG QL_MAINTAINER="whyour"
LABEL maintainer="${QL_MAINTAINER}"
@@ -51,7 +50,8 @@ EXPOSE 5700
COPY --from=builder /opt/node-global/lib/node_modules/. /usr/local/lib/node_modules/
COPY --from=builder /opt/node-global/bin/. /usr/local/bin/
RUN set -x \
RUN --mount=type=bind,from=node-runtime-amd64,source=/usr/local/bin/node,target=/tmp/node-runtime-amd64 \
set -x \
&& apk update -f \
&& apk upgrade \
&& apk --no-cache add -f bash \
@@ -70,9 +70,9 @@ RUN set -x \
unzip \
npm \
&& if [ "$TARGETARCH" = amd64 ]; then \
mv /tmp/node-runtime-amd64 /usr/local/bin/node; \
cp /tmp/node-runtime-amd64 /usr/local/bin/node; \
ln -sf /usr/local/bin/node /usr/bin/node; \
else rm /tmp/node-runtime-amd64; fi \
fi \
&& rm -rf /var/cache/apk/* \
&& apk update \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
+4 -4
View File
@@ -29,7 +29,6 @@ FROM python:3.10-alpine
ARG TARGETARCH
ARG TARGETVARIANT
COPY --from=node-runtime-amd64 /usr/local/bin/node /tmp/node-runtime-amd64
ARG QL_MAINTAINER="whyour"
LABEL maintainer="${QL_MAINTAINER}"
@@ -51,7 +50,8 @@ EXPOSE 5700
COPY --from=builder /opt/node-global/lib/node_modules/. /usr/local/lib/node_modules/
COPY --from=builder /opt/node-global/bin/. /usr/local/bin/
RUN set -x \
RUN --mount=type=bind,from=node-runtime-amd64,source=/usr/local/bin/node,target=/tmp/node-runtime-amd64 \
set -x \
&& apk update -f \
&& apk upgrade \
&& apk --no-cache add -f bash \
@@ -70,9 +70,9 @@ RUN set -x \
unzip \
npm \
&& if [ "$TARGETARCH" = amd64 ]; then \
mv /tmp/node-runtime-amd64 /usr/local/bin/node; \
cp /tmp/node-runtime-amd64 /usr/local/bin/node; \
ln -sf /usr/local/bin/node /usr/bin/node; \
else rm /tmp/node-runtime-amd64; fi \
fi \
&& rm -rf /var/cache/apk/* \
&& apk update \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
+44
View File
@@ -0,0 +1,44 @@
"""Check serialized Docker layers, including files hidden by later deletions."""
import hashlib
import json
import sys
import tarfile
archive, architecture = sys.argv[1:]
assert architecture in {"amd64", "arm"}
nodes = []
with tarfile.open(archive) as image:
manifests = json.load(image.extractfile("manifest.json"))
assert len(manifests) == 1, "Expected one platform image"
manifest = manifests[0]
config = json.load(image.extractfile(manifest["Config"]))
assert config["architecture"] == architecture
for name in manifest["Layers"]:
with tarfile.open(fileobj=image.extractfile(name), mode="r|*") as layer:
for member in layer:
filename = member.name.removeprefix("./")
assert not (
filename == "tmp/node-runtime-amd64" and member.size > 0
), "Temporary amd64 Node remains in an immutable layer"
if filename not in {"usr/local/bin/node", "usr/bin/node"}:
continue
if not member.isfile():
continue
with layer.extractfile(member) as binary:
header = binary.read(20)
assert header[:4] == b"\x7fELF", "Expected a native Node binary"
digest = hashlib.sha256(header)
while chunk := binary.read(1024 * 1024):
digest.update(chunk)
endian = "little" if header[5] == 1 else "big"
machine = int.from_bytes(header[18:20], endian)
assert machine == {"amd64": 62, "arm": 40}[architecture], (
"Wrong Node architecture in an image layer"
)
nodes.append({"path": filename, "size": member.size, "sha256": digest.hexdigest()})
assert len(nodes) == 1, "Expected exactly one retained Node binary across all layers"
assert nodes[0]["path"] == (
"usr/local/bin/node" if architecture == "amd64" else "usr/bin/node"
)
print(json.dumps({"pass": True, "architecture": architecture, "layers": len(manifest["Layers"]), "nodes": nodes}))