This is an automated email from the ASF dual-hosted git repository.
nytai pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 0ccb586540a fix(docker): use the system CA store for build-time
downloads (#45678)
0ccb586540a is described below
commit 0ccb586540a50b780936789510f6503d37eeafc9
Author: ʈᵃᵢ <[email protected]>
AuthorDate: Sun Oct 25 17:51:23 2026 -0700
fix(docker): use the system CA store for build-time downloads (#45678)
---
.github/renovate.json | 40 ++++++++++++++++++
.github/workflows/docker.yml | 16 ++++++--
Dockerfile | 49 +++++++++++++++++++----
scripts/docker-build-extra-flags.sh | 4 +-
tests/unit_tests/scripts/docker_cache_ref_test.py | 3 +-
5 files changed, 99 insertions(+), 13 deletions(-)
diff --git a/.github/renovate.json b/.github/renovate.json
new file mode 100644
index 00000000000..494b16e058c
--- /dev/null
+++ b/.github/renovate.json
@@ -0,0 +1,40 @@
+{
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
+ "description": "Renovate only refreshes the pinned base-image digests in the
root Dockerfile; Dependabot (.github/dependabot.yml) owns npm, pip and GitHub
Actions updates.",
+ "extends": ["config:recommended", "docker:pinDigests"],
+ "enabledManagers": ["dockerfile", "custom.regex"],
+ "includePaths": ["Dockerfile"],
+ "labels": ["dependencies", "docker"],
+ "schedule": ["* 0-5 * * 1"],
+ "packageRules": [
+ {
+ "description": "The python image is referenced as `FROM
python:${PY_VER}`, so its tag and digest live in the PY_VER ARG; leave it to
the custom manager below so the dockerfile manager can't propose a competing
update.",
+ "matchManagers": ["dockerfile"],
+ "matchDepNames": ["python"],
+ "enabled": false
+ },
+ {
+ "description": "Base image tags (Node major, PY_VER's Python line,
Debian release) are deliberate choices, not routine bumps; see
python-nightly-canary.yml for how PY_VER moves. Only refresh the digest behind
the current tag.",
+ "matchManagers": ["dockerfile", "custom.regex"],
+ "matchUpdateTypes": ["major", "minor", "patch"],
+ "enabled": false
+ },
+ {
+ "description": "Refresh all base-image digests in one weekly PR so the
Node and Python stages are rebuilt together.",
+ "matchManagers": ["dockerfile", "custom.regex"],
+ "matchUpdateTypes": ["digest", "pinDigest"],
+ "groupName": "Docker base image digests"
+ }
+ ],
+ "customManagers": [
+ {
+ "customType": "regex",
+ "description": "Track the digest pinned in the Dockerfile's PY_VER ARG
(tag@sha256:...), which FROM python:${PY_VER} consumes.",
+ "managerFilePatterns": ["/^Dockerfile$/"],
+ "matchStrings": ["ARG
PY_VER=(?<currentValue>[^@\\s]+)@(?<currentDigest>sha256:[a-f0-9]{64})"],
+ "depNameTemplate": "python",
+ "datasourceTemplate": "docker",
+ "versioningTemplate": "docker"
+ }
+ ]
+}
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 052dfbe1e6b..2ee6cc90351 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -142,7 +142,9 @@ jobs:
local preset="$1" expected="$2" extra_flags command
extra_flags="$(scripts/docker-build-extra-flags.sh "$preset"
dummy-tag)"
command="$(docker_command "$preset" "$extra_flags")"
- if ! grep -Fq --
"--cache-from=type=registry,ref=apache/superset-cache:$expected" <<<"$command";
then
+ # Match the whole flag, so a ref with extra trailing text (e.g. a
+ # digest) doesn't pass on its prefix.
+ if ! grep -oE -- '--cache-from=[^[:space:]]+' <<<"$command" | grep
-Fxq -- "--cache-from=type=registry,ref=apache/superset-cache:$expected"; then
echo "::error::preset '$preset' expected cache-from
apache/superset-cache:$expected (full command: $command)"
exit 1
fi
@@ -196,7 +198,7 @@ jobs:
load_build_plan push refs/heads/master master
extra_flags="$(PUBLISH_DOCKER_CACHE=true DOCKERHUB_TOKEN=dummy
scripts/docker-build-extra-flags.sh "$preset" dummy-tag)"
command="$(docker_command "$preset" "$extra_flags" --context
"$BUILD_CONTEXT" --context-ref "$BUILD_CONTEXT_REF")"
- if ! grep -Fq --
"--cache-to=type=registry,mode=max,ref=apache/superset-cache:$expected"
<<<"$command"; then
+ if ! grep -oE -- '--cache-to=[^[:space:]]+' <<<"$command" | grep
-Fxq --
"--cache-to=type=registry,mode=max,ref=apache/superset-cache:$expected"; then
echo "::error::preset '$preset' expected cache-to
apache/superset-cache:$expected for master publish (full command: $command)"
exit 1
fi
@@ -255,8 +257,16 @@ jobs:
assert_publish_guard push refs/heads/7.0 aaaa111 bbbb222 true
assert_publish_guard pull_request refs/pull/44250/merge aaaa111
bbbb222 true
assert_supersetbot_exports_cache_with_token superset
+ # The default presets must pass the Dockerfile's digest-pinned PY_VER
+ # (tag@sha256:...) through unchanged; read it the way
+ # docker-build-extra-flags.sh does so digest bumps need no edit here.
+ default_py_ver="$(sed -n 's/^ARG PY_VER=//p' Dockerfile | head -n 1)"
+ if [[ "$default_py_ver" != *@sha256:* ]]; then
+ echo "::error::Dockerfile ARG PY_VER must pin a digest
(tag@sha256:...), got: ${default_py_ver:-<none>}"
+ exit 1
+ fi
for preset in superset dev lean; do
- assert_effective_py_ver "$preset" "3.11-slim-trixie"
+ assert_effective_py_ver "$preset" "$default_py_ver"
expected_cache="3.11-slim-trixie"
if [ "$preset" != "superset" ]; then
expected_cache="${expected_cache}-${preset}"
diff --git a/Dockerfile b/Dockerfile
index 6d62c0d01fe..a3b02332eb0 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -18,7 +18,12 @@
######################################################################
# Node stage to deal with static asset construction
######################################################################
-ARG PY_VER=3.11-slim-trixie
+# Base images are pinned by digest so builds are reproducible and cached
+# stages don't silently outlive base image updates. Renovate
+# (.github/renovate.json) refreshes the digests weekly; when changing a tag
+# by hand, bump its digest too. PY_VER carries its digest, so overriding it
+# (e.g. the py311/py312 presets) replaces both.
+ARG
PY_VER=3.11-slim-trixie@sha256:b7a9e5f6a93d6d4a31f129bbd4bdee69efccb6ded0fb230d78aafaf37af98377
# If BUILDPLATFORM is null, set it to 'amd64' (or leave as is otherwise).
ARG BUILDPLATFORM=${BUILDPLATFORM:-amd64}
@@ -26,6 +31,14 @@ ARG BUILDPLATFORM=${BUILDPLATFORM:-amd64}
# Include translations in the final build
ARG BUILD_TRANSLATIONS="false"
+######################################################################
+# system-ca exposes the system CA bundle of the pinned python image. The
+# node base images ship no CA store, so the Node stages copy this one in
+# and verify TLS against the same store as the Python stages, rather than
+# Node's built-in CA list.
+######################################################################
+FROM python:${PY_VER} AS system-ca
+
######################################################################
# superset-node-manifests holds only the npm manifests of the frontend
# tree (the root package.json + lockfile, .npmrc, and every workspace
package.json),
@@ -34,7 +47,7 @@ ARG BUILD_TRANSLATIONS="false"
# Docker cannot COPY a glob while keeping the directory structure, hence
# the copy-then-prune stage.
######################################################################
-FROM --platform=${BUILDPLATFORM} node:24-trixie-slim AS superset-node-manifests
+FROM --platform=${BUILDPLATFORM}
node:24-trixie-slim@sha256:43b9ba1142b8dcf33fa8f922ca1be85714adc444e0586c7cf7726daaeabc1bea
AS superset-node-manifests
WORKDIR /app/superset-frontend
COPY superset-frontend /app/superset-frontend
RUN find . -type f ! -name package.json ! -name package-lock.json ! -name
.npmrc -delete \
@@ -43,7 +56,7 @@ RUN find . -type f ! -name package.json ! -name
package-lock.json ! -name .npmrc
######################################################################
# superset-node-ci used as a base for building frontend assets and CI
######################################################################
-FROM --platform=${BUILDPLATFORM} node:24-trixie-slim AS superset-node-ci
+FROM --platform=${BUILDPLATFORM}
node:24-trixie-slim@sha256:43b9ba1142b8dcf33fa8f922ca1be85714adc444e0586c7cf7726daaeabc1bea
AS superset-node-ci
ARG BUILD_TRANSLATIONS
ENV BUILD_TRANSLATIONS=${BUILD_TRANSLATIONS}
ARG DEV_MODE="false" # Skip frontend build in dev mode
@@ -76,6 +89,11 @@ ENV npm_config_fetch_retries=5 \
npm_config_fetch_retry_maxtimeout=120000 \
npm_config_fetch_timeout=600000
+# Verify TLS against the system CA store (see system-ca)
+COPY --from=system-ca /etc/ssl/certs/ca-certificates.crt
/etc/ssl/certs/ca-certificates.crt
+ENV NODE_OPTIONS=--use-openssl-ca \
+ SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
+
# Install dependencies from the manifests alone (root + workspaces) if not in
# dev mode. With the workspace package.json files present, npm ci links the
# workspaces and installs their dependencies too, so the CI jobs that run
@@ -120,7 +138,7 @@ RUN if [ "${BUILD_TRANSLATIONS}" = "true" ]; then \
# superset-websocket builds the realtime WebSocket (Node) server that
# ships in the official image, launched via docker/entrypoints/run-websocket.sh
######################################################################
-FROM node:24-trixie-slim AS superset-websocket
+FROM
node:24-trixie-slim@sha256:43b9ba1142b8dcf33fa8f922ca1be85714adc444e0586c7cf7726daaeabc1bea
AS superset-websocket
# Harden `npm ci` against transient npm-registry network blips (e.g.
ECONNRESET).
ENV npm_config_fetch_retries=5 \
@@ -128,6 +146,11 @@ ENV npm_config_fetch_retries=5 \
npm_config_fetch_retry_maxtimeout=120000 \
npm_config_fetch_timeout=600000
+# Verify TLS against the system CA store (see system-ca)
+COPY --from=system-ca /etc/ssl/certs/ca-certificates.crt
/etc/ssl/certs/ca-certificates.crt
+ENV NODE_OPTIONS=--use-openssl-ca \
+ SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
+
WORKDIR /app/superset-websocket
# Install against the lockfile first (cached until it changes), then bundle the
@@ -156,7 +179,12 @@ RUN useradd --user-group -d ${SUPERSET_HOME} -m
--no-log-init --shell /bin/bash
# Some bash scripts needed throughout the layers
COPY --chmod=755 docker/*.sh /app/docker/
-RUN pip install --no-cache-dir --upgrade uv
+# Use the system CA store for every download in the Python stages, rather than
+# the CA lists pip and uv bundle. UV_SYSTEM_CERTS needs uv>=0.11.0.
+ENV PIP_CERT=/etc/ssl/certs/ca-certificates.crt \
+ UV_SYSTEM_CERTS=true
+
+RUN pip install --no-cache-dir --upgrade "uv>=0.11.0"
# Using uv as it's faster/simpler than pip
RUN uv venv /app/.venv
@@ -207,7 +235,9 @@ RUN mkdir -p \
requirements \
&& touch superset/static/version_info.json
-# Optionally install Playwright and the headless Chromium used for screenshots
+# Optionally install Playwright and the headless Chromium used for screenshots.
+# `playwright install` runs on Playwright's bundled Node; --use-openssl-ca
makes
+# it use the system CA store, consistent with the rest of the build.
ENV PLAYWRIGHT_BROWSERS_PATH=/usr/local/share/playwright-browsers
ARG INCLUDE_CHROMIUM="false"
@@ -215,7 +245,8 @@ RUN --mount=type=cache,target=${SUPERSET_HOME}/.cache/uv \
if [ "${INCLUDE_CHROMIUM}" = "true" ]; then \
uv pip install playwright && \
playwright install-deps && \
- playwright install chromium; \
+ NODE_OPTIONS=--use-openssl-ca
SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \
+ playwright install chromium; \
else \
echo "Skipping browser installation"; \
fi
@@ -328,10 +359,12 @@ RUN --mount=type=cache,target=${SUPERSET_HOME}/.cache/uv \
# INCLUDE_CHROMIUM build arg on python-common) because this stage builds
# FROM lean, which is already past that arg. Chromium ships for both
# linux/amd64 and linux/arm64, so this works on multi-platform builds.
+# See the INCLUDE_CHROMIUM step in python-common for --use-openssl-ca.
RUN --mount=type=cache,target=${SUPERSET_HOME}/.cache/uv \
uv pip install playwright \
&& playwright install-deps \
- && playwright install chromium
+ && NODE_OPTIONS=--use-openssl-ca
SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \
+ playwright install chromium
USER superset
diff --git a/scripts/docker-build-extra-flags.sh
b/scripts/docker-build-extra-flags.sh
index 6b5bd68f46f..cccfc9f7ceb 100755
--- a/scripts/docker-build-extra-flags.sh
+++ b/scripts/docker-build-extra-flags.sh
@@ -42,6 +42,8 @@ BUILD_PRESET="${1:?usage: docker-build-extra-flags.sh
<build_preset> [image_tag]
IMAGE_TAG="${2:-}"
BUILD_MODE="${3:-ci}"
DEFAULT_PY_VER="$(sed -n 's/^ARG PY_VER=//p' Dockerfile | head -n 1)"
+# PY_VER pins a digest (tag@sha256:...); registry cache refs take the tag only
+DEFAULT_PY_TAG="${DEFAULT_PY_VER%%@*}"
if [ -z "$DEFAULT_PY_VER" ]; then
echo "Could not determine the default PY_VER from Dockerfile" >&2
exit 1
@@ -65,7 +67,7 @@ case "$BUILD_PRESET" in
CACHE_REF="apache/superset-cache:3.12-slim-bookworm"
;;
*)
- CACHE_REF="apache/superset-cache:${DEFAULT_PY_VER}"
+ CACHE_REF="apache/superset-cache:${DEFAULT_PY_TAG}"
# Keep the superset cache ref for Compose consumers; isolate other
# matrix targets so concurrent exports cannot overwrite its layers.
if [ "$BUILD_PRESET" != "superset" ]; then
diff --git a/tests/unit_tests/scripts/docker_cache_ref_test.py
b/tests/unit_tests/scripts/docker_cache_ref_test.py
index 573321d1f52..742ae7508de 100644
--- a/tests/unit_tests/scripts/docker_cache_ref_test.py
+++ b/tests/unit_tests/scripts/docker_cache_ref_test.py
@@ -59,7 +59,8 @@ def _dockerfile_py_ver() -> str:
"""
for line in (REPO_ROOT / "Dockerfile").read_text().splitlines():
if line.startswith("ARG PY_VER="):
- return line.removeprefix("ARG PY_VER=").strip()
+ # PY_VER pins a digest (tag@sha256:...); cache refs use the tag
+ return line.removeprefix("ARG PY_VER=").strip().split("@", 1)[0]
raise AssertionError("Dockerfile no longer declares a default ARG PY_VER")