You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by po...@apache.org on 2026/10/20 13:34:23 UTC
(airflow) branch main updated: Base CI and PROD images on hardened Python images (#73038)
This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 359d361de93 Base CI and PROD images on hardened Python images (#73038)
359d361de93 is described below
commit 359d361de93f8feac751788baf6934faa65a0c5c
Author: Jarek Potiuk <ja...@potiuk.com>
AuthorDate: Tue Oct 20 15:34:13 2026 +0200
Base CI and PROD images on hardened Python images (#73038)
* Base CI and PROD images on hardened Python images
Squashed from 19 commits (backup branch: backup/hardened-python-base-images-pre-rebase) before rebasing onto main.
Generated-by: Claude Opus 5
Claude-Session: https://claude.ai/code/session_014np1fVAxHmGaBFt3qebJ8x
* Base images on trixie and keep a legacy bookworm PROD image flavor
The 3.4.x line keeps publishing the previous images - Python compiled from sources on bookworm - as a deprecated legacy flavor, so the PROD image has to be buildable both ways while the CI image only needs the hardened trixie one. A weekly canary builds and checks the legacy images so they do not rot between releases.
Generated-by: Claude Opus 5
Claude-Session: https://claude.ai/code/session_014np1fVAxHmGaBFt3qebJ8x
* Support the trixie hardened images' Debian-packaged Python and test legacy images like regular ones
The Debian 13 hardened images install Python as Debian packages under /usr instead of /opt/python, so the image has to find the base Python's prefix and the system-Python guard must not mistake those packages for Debian's own Python. The legacy canary now runs the regular PROD image tests, not just a smoke check, so the legacy images are covered as well as the default ones.
Generated-by: Claude Opus 5
Claude-Session: https://claude.ai/code/session_014np1fVAxHmGaBFt3qebJ8x
* Drop CI image packages that Debian trixie no longer has
The CI image is trixie-only now: libgcc-11-dev was a bookworm-era pin (build-essential brings the matching libgcc-14-dev) and software-properties-common was removed in Debian 13; nothing uses its add-apt-repository.
Generated-by: Claude Opus 5
Claude-Session: https://claude.ai/code/session_014np1fVAxHmGaBFt3qebJ8x
* Trust Microsoft's newer signing key for the trixie MSSQL repository
Microsoft signs its Debian 13 package repository with its 2025 general signing key rather than the 2015 release key, so apt on trixie rejected the repository as unsigned.
Generated-by: Claude Opus 5
Claude-Session: https://claude.ai/code/session_014np1fVAxHmGaBFt3qebJ8x
* Restore the PAM configs the trixie hardened images strip
The Debian 13 hardened images ship libpam-runtime and passwd already installed but without their /etc/pam.d files, so installing them restored nothing and adding the CI image's airflow user failed with pam_start() errors.
Generated-by: Claude Opus 5
Claude-Session: https://claude.ai/code/session_014np1fVAxHmGaBFt3qebJ8x
* Document the anonymous Debian package repository of the hardened images
The trixie hardened images add Docker's own package repository, whose rebuilt packages take precedence over Debian's - users extending the image should know where their packages come from, and that no credentials are needed for it.
Generated-by: Claude Opus 5
Claude-Session: https://claude.ai/code/session_014np1fVAxHmGaBFt3qebJ8x
* Keep a second hardened Python out of the trixie images
On trixie, libenchant-2-dev's build dependencies need python3, which Docker's package repository satisfies with its own python-3.13 - a second interpreter in the 3.12 image that broke building C extensions. pyenchant only needs the runtime library, which it finds through the ldconfig cache, and the guard now fails early on any hardened Python other than the image's own.
Generated-by: Claude Opus 5
Claude-Session: https://claude.ai/code/session_014np1fVAxHmGaBFt3qebJ8x
* Install the Python build dependencies for the legacy image flavor
Dropping the packages that only existed to compile Python left the legacy flavor compiling a Python without sqlite3, lzma or readline - CPython builds those modules only when their headers are present, and the image could not even run airflow. The build now also fails when one of them is missing.
Generated-by: Claude Opus 5
Claude-Session: https://claude.ai/code/session_014np1fVAxHmGaBFt3qebJ8x
* Refresh the shared library cache after apt runs in hardened images
The hardened images ship libc-bin without its dpkg trigger, so libraries installed by apt stayed invisible to ctypes.util.find_library() - the CI image's docs build failed with "The 'enchant' C library was not found". A dpkg post-invoke hook runs ldconfig after every apt run, also in images extending ours.
Generated-by: Claude Opus 5
Claude-Session: https://claude.ai/code/session_014np1fVAxHmGaBFt3qebJ8x
* Warn about apt pulling a second Python into hardened images
Extending a hardened image with apt packages that depend on python3 silently installs another Python next to the image's own, and C extensions then build against the wrong one - users need to know to check before adding such packages.
Generated-by: Claude Opus 5
Claude-Session: https://claude.ai/code/session_014np1fVAxHmGaBFt3qebJ8x
* Add trixie and sigstore to the docs spelling wordlist
Generated-by: Claude Opus 5
Claude-Session: https://claude.ai/code/session_014np1fVAxHmGaBFt3qebJ8x
* Count only host keys when waiting for sshd in the CI image
OpenSSH 10 in trixie prints a '# host:port' banner comment per connection to ssh-keyscan's stdout, so the line count never matched the three host keys and every test job waited until it timed out.
Generated-by: Claude Opus 5
Claude-Session: https://claude.ai/code/session_014np1fVAxHmGaBFt3qebJ8x
* Invalidate the CI image mount cache for the trixie base
The uv cache restored into CI image builds held wheels compiled against bookworm's libraries - an xmlsec wheel built against bookworm's MD5-enabled libxmlsec1 failed to import on trixie, which dropped MD5. The cache key does not encode the base OS, so it has to be bumped when that changes.
Generated-by: Claude Opus 5
Claude-Session: https://claude.ai/code/session_014np1fVAxHmGaBFt3qebJ8x
* Install Java that exists on trixie in the Java examples
Debian trixie ships no Java 17. The Java SDK e2e image keeps Java 17 for Spark 3.5 by taking it from the Temurin repository, and the docs example uses trixie's Java 21.
Generated-by: Claude Opus 5
Claude-Session: https://claude.ai/code/session_014np1fVAxHmGaBFt3qebJ8x
* Let old C extensions build in the trixie CI image
GCC 14 turns incompatible pointer types, integer conversions and implicit declarations into errors, and the hardened Python passes -Werror=implicit-function-declaration on to extensions - the lowest-dependency tests' old sdists (xmlsec 1.3.14, ibm-db 3.2.0) only built because those were warnings.
Generated-by: Claude Opus 5
Claude-Session: https://claude.ai/code/session_014np1fVAxHmGaBFt3qebJ8x
* Document the stricter C extension builds of the hardened images
Users extending the image with old source distributions hit the same GCC 14 errors as CI, and need to know how to relax them.
Generated-by: Claude Opus 5
Claude-Session: https://claude.ai/code/session_014np1fVAxHmGaBFt3qebJ8x
* Export KinD logs when the lang-SDK K8S test setup fails
A failing lang-SDK helm upgrade raised past the log export that every other step of the complete K8S test run does, so the cluster was deleted with nothing saved and the job's KinD logs artifact stayed empty.
Generated-by: Claude Opus 5
Claude-Session: https://claude.ai/code/session_014np1fVAxHmGaBFt3qebJ8x
* Keep apt from replacing the hardened images' Python
Debian 13's python3 packages conflict with the hardened images' python-3.X
packages, so installing NodeSource's nodejs (which depends on python3) into
the lang-SDK runtime image made apt remove the image's Python 3.11, and the
migration pods crash-looped. The image now holds its Python packages, so such
an install fails instead, and the runtime image copies the node binary from
the official node image rather than installing the deb.
Generated-by: Claude Opus 5
* Keep perl-base in step with the perl apt can install
The hardened apt repository publishes git, which depends on perl, but not perl
itself, while the hardened images ship their own build of perl-base. Debian's
perl depends on exactly its own perl-base version, so apt refused to install git
and the hardened CI image build failed. The image now moves perl-base to the
version of the installable perl before installing its packages; when the
hardened repository publishes a matching perl, the versions agree and nothing
changes.
Generated-by: Claude Opus 5
* Mirror the hardened base images only from main
The mirror is the default BASE_IMAGE of every Airflow build, and the job holds packages:write plus the Docker Hub credentials. Scheduled runs are always on main, but anyone with write access could dispatch the workflow from another branch and replace the mirrored images.
Generated-by: Claude Opus 5
* Check the Adoptium signing key fingerprint in the Java e2e image
The key is downloaded over HTTPS at build time; pinning its fingerprint means a replaced key on the server fails the build instead of being trusted for every package from that repository.
Generated-by: Claude Opus 5
* Allow mirroring the hardened base images from v3-*-test branches
The release branches build images from the same mirror, so maintainers need to refresh it from there too.
Generated-by: Claude Opus 5
---
.../actions/build_ci_image_with_cache/action.yml | 10 +-
.github/workflows/ci-image-build.yml | 19 +-
.github/workflows/mirror-hardened-base-images.yml | 80 ++++
.github/workflows/prod-image-build.yml | 9 +-
.github/workflows/push-image-cache.yml | 4 +-
.../scheduled-legacy-prod-image-canary.yml | 262 ++++++++++++
Dockerfile | 443 ++++++++++++++++-----
Dockerfile.ci | 435 +++++++++++++++-----
airflow-core/newsfragments/73038.significant.rst | 31 ++
airflow-e2e-tests/docker/Dockerfile.java | 23 +-
dev/breeze/doc/09_release_management_tasks.rst | 28 ++
dev/breeze/doc/ci/02_images.md | 20 +-
dev/breeze/doc/images/output_ci-image_build.svg | 94 ++---
dev/breeze/doc/images/output_ci-image_build.txt | 2 +-
dev/breeze/doc/images/output_prod-image_build.svg | 164 ++++----
dev/breeze/doc/images/output_prod-image_build.txt | 2 +-
.../doc/images/output_release-management.svg | 38 +-
.../doc/images/output_release-management.txt | 2 +-
...utput_release-management_mirror-base-images.svg | 116 ++++++
...utput_release-management_mirror-base-images.txt | 1 +
.../output_setup_check-all-params-in-groups.svg | 20 +-
.../output_setup_check-all-params-in-groups.txt | 2 +-
.../output_setup_regenerate-command-images.svg | 2 +-
.../output_setup_regenerate-command-images.txt | 2 +-
.../airflow_breeze/commands/ci_image_commands.py | 4 -
.../commands/ci_image_commands_config.py | 1 -
.../commands/common_image_options.py | 10 +
.../airflow_breeze/commands/kubernetes_commands.py | 12 +-
.../commands/production_image_commands.py | 4 +
.../commands/production_image_commands_config.py | 1 +
.../commands/release_management_commands.py | 92 ++++-
.../commands/release_management_commands_config.py | 9 +
dev/breeze/src/airflow_breeze/global_constants.py | 32 +-
.../src/airflow_breeze/params/build_prod_params.py | 11 +
.../airflow_breeze/params/common_build_params.py | 6 +-
dev/breeze/tests/test_build_prod_params.py | 82 ++++
.../tests/test_kubernetes_lang_sdk_commands.py | 45 +++
dev/breeze/tests/test_mirror_base_images.py | 98 +++++
docker-stack-docs/build-arg-ref.rst | 30 +-
docker-stack-docs/build.rst | 128 +++++-
docker-stack-docs/changelog.rst | 43 ++
.../customizing/add-build-essential-custom.sh | 2 +-
.../docker-examples/customizing/custom-sources.sh | 2 +-
.../customizing/pypi-dev-runtime-deps.sh | 2 +-
.../customizing/pypi-extras-and-deps.sh | 2 +-
.../customizing/pypi-selected-version.sh | 2 +-
.../extending/add-providers/Dockerfile | 4 +-
.../restricted/restricted_environments.sh | 2 +-
docs/spelling_wordlist.txt | 3 +
kubernetes-tests/lang_sdk/Dockerfile.runtimes | 23 +-
scripts/ci/prek/update_docker_gpg_keys.py | 2 +
scripts/ci/prek/upgrade_important_versions.py | 82 ++--
scripts/docker/common.sh | 16 +-
scripts/docker/entrypoint_ci.sh | 5 +-
scripts/docker/install_mssql.sh | 4 +-
scripts/docker/install_mysql.sh | 17 +-
scripts/docker/install_os_dependencies.sh | 353 ++++++++++++----
scripts/docker/install_postgres.sh | 2 +-
scripts/docker/keys/microsoft-2025.asc | 29 ++
scripts/in_container/run_ci_tests.sh | 11 +-
.../ci/prek/test_upgrade_important_versions.py | 62 +++
61 files changed, 2451 insertions(+), 591 deletions(-)
diff --git a/.github/actions/build_ci_image_with_cache/action.yml b/.github/actions/build_ci_image_with_cache/action.yml
index b7a6d0bbce4..0d6e1afa889 100644
--- a/.github/actions/build_ci_image_with_cache/action.yml
+++ b/.github/actions/build_ci_image_with_cache/action.yml
@@ -113,7 +113,7 @@ runs:
# yamllint disable-line rule:line-length
uses: apache/infrastructure-actions/stash/restore@61dcea11f19e2bbe1263f14d72235e8da17d3ad0 # restore/v1.0.0
with:
- key: "ci-cache-mount-save-v3-${{ inputs.platform }}-${{ inputs.python }}\
+ key: "ci-cache-mount-save-v4-${{ inputs.platform }}-${{ inputs.python }}\
-${{ inputs.image-stash-ref }}"
path: "/tmp/"
id: restore-mount-cache
@@ -122,7 +122,7 @@ runs:
shell: bash
env:
PYTHON_MAJOR_MINOR_VERSION: ${{ inputs.python }}
- CACHE_FILE: "/tmp/ci-cache-mount-save-v3-${{ inputs.python }}.tar.gz"
+ CACHE_FILE: "/tmp/ci-cache-mount-save-v4-${{ inputs.python }}.tar.gz"
run: |
if [[ ! -f "${CACHE_FILE}" ]]; then
echo "${CACHE_FILE} is missing - the stash restore may have timed out. Building without it."
@@ -211,14 +211,14 @@ runs:
PYTHON_MAJOR_MINOR_VERSION: ${{ inputs.python }}
run: >
breeze ci-image export-mount-cache
- --cache-file /tmp/ci-cache-mount-save-v3-${PYTHON_MAJOR_MINOR_VERSION}.tar.gz
+ --cache-file /tmp/ci-cache-mount-save-v4-${PYTHON_MAJOR_MINOR_VERSION}.tar.gz
if: steps.stashed-image.outputs.reusable != 'true'
- name: "Stash the mount cache"
uses: apache/infrastructure-actions/stash/save@61dcea11f19e2bbe1263f14d72235e8da17d3ad0 # save/v1.0.0
with:
- key: "ci-cache-mount-save-v3-${{ inputs.platform }}-${{ inputs.python }}\
+ key: "ci-cache-mount-save-v4-${{ inputs.platform }}-${{ inputs.python }}\
-${{ inputs.image-stash-ref }}"
- path: "/tmp/ci-cache-mount-save-v3-${{ inputs.python }}.tar.gz"
+ path: "/tmp/ci-cache-mount-save-v4-${{ inputs.python }}.tar.gz"
if-no-files-found: 'error'
retention-days: '6'
if: steps.stashed-image.outputs.reusable != 'true'
diff --git a/.github/workflows/ci-image-build.yml b/.github/workflows/ci-image-build.yml
index 31241dd059d..6c852826590 100644
--- a/.github/workflows/ci-image-build.yml
+++ b/.github/workflows/ci-image-build.yml
@@ -61,10 +61,6 @@ on: # yamllint disable-line rule:truthy
description: "Whether to upload mount-cache artifact"
required: true
type: string
- debian-version:
- description: "Base Debian distribution to use for the build (bookworm)"
- type: string
- default: "bookworm"
install-mysql-client-type:
description: "MySQL client type to use during build (mariadb/mysql)"
type: string
@@ -219,8 +215,11 @@ jobs:
${{ inputs.image-stash-ref != '' && format('for ref {0}', inputs.image-stash-ref) || '' }}
# yamllint disable-line rule:line-length
uses: apache/infrastructure-actions/stash/restore@c968777141a2afc98d1a09c9f6979455bfebaae3 # restore/v1.0.1
+ # The mount cache holds wheels compiled against the image's system libraries - bump the "vN"
+ # in this key (and in build_ci_image_with_cache) whenever the base OS changes, or wheels built
+ # for the old one get reused (v4: bookworm -> trixie, whose libxmlsec1 has no MD5 support).
with:
- key: "ci-cache-mount-save-v3-${{ inputs.platform }}-${{ env.PYTHON_MAJOR_MINOR_VERSION }}\
+ key: "ci-cache-mount-save-v4-${{ inputs.platform }}-${{ env.PYTHON_MAJOR_MINOR_VERSION }}\
${{ inputs.image-stash-ref != '' && format('-{0}', inputs.image-stash-ref) || '' }}"
path: "/tmp/"
id: restore-cache-mount
@@ -228,7 +227,7 @@ jobs:
- name: "Verify ci-cache file exists"
if: steps.restore-cache-mount.outputs.stash-hit == 'true'
env:
- CACHE_FILE: "/tmp/ci-cache-mount-save-v3-${{ env.PYTHON_MAJOR_MINOR_VERSION }}.tar.gz"
+ CACHE_FILE: "/tmp/ci-cache-mount-save-v4-${{ env.PYTHON_MAJOR_MINOR_VERSION }}.tar.gz"
run: |
if [ ! -f "$CACHE_FILE" ]; then
echo "Cache file not found: $CACHE_FILE"
@@ -242,7 +241,7 @@ jobs:
PYTHON_MAJOR_MINOR_VERSION: ${{ env.PYTHON_MAJOR_MINOR_VERSION }}
run: >
breeze ci-image import-mount-cache
- --cache-file /tmp/ci-cache-mount-save-v3-${PYTHON_MAJOR_MINOR_VERSION}.tar.gz
+ --cache-file /tmp/ci-cache-mount-save-v4-${PYTHON_MAJOR_MINOR_VERSION}.tar.gz
if: steps.restore-cache-mount.outputs.stash-hit == 'true'
- name: "Restore CI docker image ${{ inputs.platform }}:${{ env.PYTHON_MAJOR_MINOR_VERSION }}"
# yamllint disable-line rule:line-length
@@ -380,7 +379,7 @@ jobs:
PYTHON_MAJOR_MINOR_VERSION: ${{ env.PYTHON_MAJOR_MINOR_VERSION }}
run: >
breeze ci-image export-mount-cache
- --cache-file /tmp/ci-cache-mount-save-v3-${PYTHON_MAJOR_MINOR_VERSION}.tar.gz
+ --cache-file /tmp/ci-cache-mount-save-v4-${PYTHON_MAJOR_MINOR_VERSION}.tar.gz
if: >
inputs.upload-mount-cache-artifact == 'true' &&
steps.stashed-image.outputs.reusable != 'true'
@@ -389,9 +388,9 @@ jobs:
${{ inputs.image-stash-ref != '' && format('for ref {0}', inputs.image-stash-ref) || '' }}
uses: apache/infrastructure-actions/stash/save@f1a487181effd0b216892001abdfb524442bb24d # save/v1.0.1
with:
- key: "ci-cache-mount-save-v3-${{ inputs.platform }}-${{ env.PYTHON_MAJOR_MINOR_VERSION }}\
+ key: "ci-cache-mount-save-v4-${{ inputs.platform }}-${{ env.PYTHON_MAJOR_MINOR_VERSION }}\
${{ inputs.image-stash-ref != '' && format('-{0}', inputs.image-stash-ref) || '' }}"
- path: "/tmp/ci-cache-mount-save-v3-${{ env.PYTHON_MAJOR_MINOR_VERSION }}.tar.gz"
+ path: "/tmp/ci-cache-mount-save-v4-${{ env.PYTHON_MAJOR_MINOR_VERSION }}.tar.gz"
if-no-files-found: 'error'
# A ref's cache is read by the next publish of that same ref, days rather than hours
# later, so it gets the retention the ref's image gets rather than the branch's.
diff --git a/.github/workflows/mirror-hardened-base-images.yml b/.github/workflows/mirror-hardened-base-images.yml
new file mode 100644
index 00000000000..b7eac54c318
--- /dev/null
+++ b/.github/workflows/mirror-hardened-base-images.yml
@@ -0,0 +1,80 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+---
+name: Mirror hardened base images
+# Copies the Docker Hardened Image (https://dhi.io) tags that Airflow images are built on into a
+# public ghcr.io repository owned by the project.
+#
+# Pulling from dhi.io requires a Docker Hub login. Requiring one to build an Airflow image would
+# mean every contributor and every CI job needs Docker Hub credentials, so the tags are mirrored
+# instead and `BASE_IMAGE` defaults to the mirror. The images are Apache-2.0 licensed, so
+# redistributing them is allowed.
+#
+# `docker buildx imagetools create` copies the multi-platform manifest between registries without
+# pulling the layers into the runner, so a refresh of all Python versions is cheap.
+on: # yamllint disable-line rule:truthy
+ workflow_dispatch:
+ schedule:
+ # Docker rebuilds the hardened images as CVEs are fixed, keeping the same tags. Re-running
+ # weekly is what picks those rebuilds up.
+ - cron: '27 4 * * 1'
+permissions:
+ contents: read
+concurrency:
+ group: mirror-hardened-base-images
+ cancel-in-progress: false
+jobs:
+ mirror:
+ name: "Mirror hardened base images"
+ runs-on: ["ubuntu-22.04"]
+ # Never runs on forks - they have neither the credentials nor the package to push to. Only main
+ # and the v3-*-test release branches may push: the mirror is the default BASE_IMAGE of every
+ # build, so a manual dispatch from any other branch must not be able to replace it.
+ if: >-
+ github.repository == 'apache/airflow' && (
+ github.ref == 'refs/heads/main' || (
+ startsWith(github.ref, 'refs/heads/v3-') && endsWith(github.ref, '-test')
+ )
+ )
+ permissions:
+ contents: read
+ packages: write
+ steps:
+ - name: "Cleanup repo"
+ shell: bash
+ run: sudo rm -rf ${GITHUB_WORKSPACE}/*
+ - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+ - name: "Install Breeze"
+ uses: ./.github/actions/breeze
+ # dhi.io accepts any Docker Hub account - the free tier covers the images we mirror - so this
+ # reuses the credentials the DockerHub release workflows already use.
+ - name: "Login to dhi.io"
+ env:
+ DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USER }}
+ DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
+ run: echo "${DOCKERHUB_TOKEN}" | docker login dhi.io -u "${DOCKERHUB_USER}" --password-stdin
+ - name: "Login to ghcr.io"
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ ACTOR: ${{ github.actor }}
+ run: echo "${GITHUB_TOKEN}" | docker login ghcr.io -u "${ACTOR}" --password-stdin
+ - name: "Mirror base images"
+ run: breeze release-management mirror-base-images
diff --git a/.github/workflows/prod-image-build.yml b/.github/workflows/prod-image-build.yml
index 447eb50a525..5f91474e235 100644
--- a/.github/workflows/prod-image-build.yml
+++ b/.github/workflows/prod-image-build.yml
@@ -61,9 +61,13 @@ on: # yamllint disable-line rule:truthy
default: "false"
type: string
debian-version:
- description: "Base Debian distribution to use for the build (bookworm)"
+ description: "Base Debian distribution to use for the build (trixie/bookworm)"
type: string
- default: "bookworm"
+ default: "trixie"
+ image-flavor:
+ description: "Image flavor - hardened (Docker Hardened Python base) or legacy (Python from sources)"
+ type: string
+ default: "hardened"
install-mysql-client-type:
description: "MySQL client type to use during build (mariadb/mysql)"
type: string
@@ -271,6 +275,7 @@ jobs:
DOCKER_CACHE: ${{ inputs.docker-cache }}
DISABLE_AIRFLOW_REPO_CACHE: ${{ inputs.disable-airflow-repo-cache }}
DEBIAN_VERSION: ${{ inputs.debian-version }}
+ IMAGE_FLAVOR: ${{ inputs.image-flavor }}
INSTALL_MYSQL_CLIENT_TYPE: ${{ inputs.install-mysql-client-type }}
UPGRADE_TO_NEWER_DEPENDENCIES: ${{ inputs.upgrade-to-newer-dependencies }}
INCLUDE_NOT_READY_PROVIDERS: "true"
diff --git a/.github/workflows/push-image-cache.yml b/.github/workflows/push-image-cache.yml
index 763f0b9c219..4d04deb09da 100644
--- a/.github/workflows/push-image-cache.yml
+++ b/.github/workflows/push-image-cache.yml
@@ -37,9 +37,9 @@ on: # yamllint disable-line rule:truthy
required: true
type: string
debian-version:
- description: "Base Debian distribution to use for the build (bookworm)"
+ description: "Base Debian distribution to use for the PROD image build (trixie/bookworm)"
type: string
- default: "bookworm"
+ default: "trixie"
install-mysql-client-type:
description: "MySQL client type to use during build (mariadb/mysql)"
type: string
diff --git a/.github/workflows/scheduled-legacy-prod-image-canary.yml b/.github/workflows/scheduled-legacy-prod-image-canary.yml
new file mode 100644
index 00000000000..19cdc30b89e
--- /dev/null
+++ b/.github/workflows/scheduled-legacy-prod-image-canary.yml
@@ -0,0 +1,262 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+---
+name: "Scheduled legacy PROD image canary"
+# The default PROD images are hardened and based on Debian trixie, and that is all that PR and canary
+# builds test. The "legacy" flavor - Python compiled from sources on Debian bookworm - is still
+# published for the 3.4.x line, so once a week this builds legacy bookworm PROD images and runs the
+# regular PROD image tests against them, instead of finding out only when a release is being prepared.
+#
+# It reuses the PROD image workflows of the main CI build. The constraints those workflows normally get
+# from the CI image are taken from the constraints branch instead, so no CI image has to be built.
+on: # yamllint disable-line rule:truthy
+ schedule:
+ - cron: '43 2 * * 0'
+ workflow_dispatch:
+permissions:
+ contents: read
+concurrency:
+ group: scheduled-legacy-prod-image-canary-${{ github.ref }}
+ cancel-in-progress: true
+jobs:
+ build-info:
+ name: "Build info"
+ runs-on: ["ubuntu-22.04"]
+ if: github.repository == 'apache/airflow' || github.event_name == 'workflow_dispatch'
+ timeout-minutes: 15
+ outputs:
+ canary-run: ${{ steps.source-run-info.outputs.canary-run }}
+ debug-resources: ${{ steps.selective-checks.outputs.debug-resources }}
+ default-branch: ${{ steps.selective-checks.outputs.default-branch }}
+ default-constraints-branch: ${{ steps.selective-checks.outputs.default-constraints-branch }}
+ default-kubernetes-version: ${{ steps.selective-checks.outputs.default-kubernetes-version }}
+ default-python-version: ${{ steps.selective-checks.outputs.default-python-version }}
+ disable-airflow-repo-cache: ${{ steps.selective-checks.outputs.disable-airflow-repo-cache }}
+ include-success-outputs: ${{ steps.selective-checks.outputs.include-success-outputs }}
+ java-sdk-version: ${{ steps.selective-checks.outputs.java-sdk-version }}
+ kubernetes-combos: ${{ steps.selective-checks.outputs.kubernetes-combos }}
+ providers-compatibility-tests-matrix: >
+ ${{ steps.selective-checks.outputs.providers-compatibility-tests-matrix }}
+ python-versions: ${{ steps.selective-checks.outputs.python-versions }}
+ python-versions-list-as-string: ${{ steps.selective-checks.outputs.python-versions-list-as-string }}
+ run-airflow-ctl-integration-tests: >-
+ ${{ steps.selective-checks.outputs.run-airflow-ctl-integration-tests }}
+ run-event-driven-e2e-tests: ${{ steps.selective-checks.outputs.run-event-driven-e2e-tests }}
+ run-go-sdk-e2e-tests: ${{ steps.selective-checks.outputs.run-go-sdk-e2e-tests }}
+ run-java-sdk-e2e-tests: ${{ steps.selective-checks.outputs.run-java-sdk-e2e-tests }}
+ run-openlineage-e2e-compat-tests: ${{ steps.selective-checks.outputs.run-openlineage-e2e-compat-tests }}
+ run-openlineage-e2e-tests: ${{ steps.selective-checks.outputs.run-openlineage-e2e-tests }}
+ run-remote-logging-elasticsearch-e2e-tests: >-
+ ${{ steps.selective-checks.outputs.run-remote-logging-elasticsearch-e2e-tests }}
+ run-remote-logging-opensearch-e2e-tests: >-
+ ${{ steps.selective-checks.outputs.run-remote-logging-opensearch-e2e-tests }}
+ run-remote-logging-s3-e2e-tests: ${{ steps.selective-checks.outputs.run-remote-logging-s3-e2e-tests }}
+ run-task-sdk-integration-tests: ${{ steps.selective-checks.outputs.run-task-sdk-integration-tests }}
+ run-ts-sdk-e2e-tests: ${{ steps.selective-checks.outputs.run-ts-sdk-e2e-tests }}
+ run-ui-e2e-tests: ${{ steps.selective-checks.outputs.run-ui-e2e-tests }}
+ use-uv: ${{ steps.selective-checks.outputs.force-pip == 'true' && 'false' || 'true' }}
+ steps:
+ - name: "Cleanup repo"
+ shell: bash
+ run: sudo rm -rf ${GITHUB_WORKSPACE}/*
+ - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ fetch-depth: 2
+ persist-credentials: false
+ - name: "Install Breeze"
+ uses: ./.github/actions/breeze
+ - name: "Save github context to file"
+ # Same as in ci-amd.yml: a scheduled or dispatched run has no attacker-controlled context, and
+ # the single-quoted heredoc disables every shell expansion of the JSON anyway.
+ shell: bash
+ run: | # zizmor: ignore[template-injection]
+ cat > "${RUNNER_TEMP}/github_context.json" << '__GITHUB_CONTEXT_END__'
+ ${{ toJson(github) }}
+ __GITHUB_CONTEXT_END__
+ - name: "Get information about the Workflow"
+ id: source-run-info
+ run: breeze ci get-workflow-info 2>> ${GITHUB_OUTPUT}
+ env:
+ SKIP_BREEZE_SELF_UPGRADE_CHECK: "true"
+ GITHUB_CONTEXT_INPUT: "${{ runner.temp }}/github_context.json"
+ - name: "Selective checks"
+ id: selective-checks
+ env:
+ COMMIT_REF: "${{ github.sha }}"
+ VERBOSE: "false"
+ GITHUB_CONTEXT_INPUT: "${{ runner.temp }}/github_context.json"
+ PLATFORM: "linux/amd64"
+ run: breeze ci selective-check 2>> ${GITHUB_OUTPUT}
+
+ constraints:
+ name: "Constraints ${{ matrix.python-version }}"
+ needs: [build-info]
+ runs-on: ["ubuntu-slim"]
+ timeout-minutes: 10
+ strategy:
+ matrix:
+ python-version: ${{ fromJSON(needs.build-info.outputs.python-versions) }}
+ env:
+ PYTHON_MAJOR_MINOR_VERSION: ${{ matrix.python-version }}
+ CONSTRAINTS_BRANCH: ${{ needs.build-info.outputs.default-constraints-branch }}
+ steps:
+ - name: "Download constraints from ${{ env.CONSTRAINTS_BRANCH }}"
+ run: |
+ base_url="https://raw.githubusercontent.com/apache/airflow/${CONSTRAINTS_BRANCH}"
+ target_dir="files/constraints-${PYTHON_MAJOR_MINOR_VERSION}"
+ mkdir -p "${target_dir}"
+ for prefix in constraints constraints-no-providers constraints-source-providers; do
+ file="${prefix}-${PYTHON_MAJOR_MINOR_VERSION}.txt"
+ curl -fsSL --retry 5 "${base_url}/${file}" -o "${target_dir}/${file}"
+ done
+ ls -l "files/constraints-${PYTHON_MAJOR_MINOR_VERSION}"
+ # Same artifact name and layout as generate-constraints.yml uploads, which is what the PROD image
+ # build downloads.
+ - name: "Upload constraint artifacts"
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: constraints-${{ matrix.python-version }}
+ path: ./files/constraints-${{ matrix.python-version }}/constraints-*.txt
+ retention-days: 7
+ if-no-files-found: error
+
+ build-prod-images:
+ name: "Build legacy bookworm PROD images"
+ needs: [build-info, constraints]
+ uses: ./.github/workflows/prod-image-build.yml
+ permissions:
+ contents: read
+ packages: read
+ with:
+ runners: '["ubuntu-22.04"]'
+ platform: "linux/amd64"
+ build-type: "Legacy"
+ debian-version: "bookworm"
+ image-flavor: "legacy"
+ push-image: "false"
+ upload-image-artifact: "true"
+ upload-package-artifact: "true"
+ python-versions: ${{ needs.build-info.outputs.python-versions }}
+ default-python-version: "${{ needs.build-info.outputs.default-python-version }}"
+ branch: ${{ needs.build-info.outputs.default-branch }}
+ use-uv: ${{ needs.build-info.outputs.use-uv }}
+ upgrade-to-newer-dependencies: "false"
+ constraints-branch: ${{ needs.build-info.outputs.default-constraints-branch }}
+ # The cache in the registry is the one of the hardened images - it would not match anyway.
+ docker-cache: "disabled"
+ disable-airflow-repo-cache: ${{ needs.build-info.outputs.disable-airflow-repo-cache }}
+ prod-image-build: "true"
+
+ verify-legacy-flavor:
+ name: "Verify legacy bookworm flavor ${{ matrix.python-version }}"
+ needs: [build-info, build-prod-images]
+ runs-on: ["ubuntu-22.04"]
+ timeout-minutes: 20
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version: ${{ fromJSON(needs.build-info.outputs.python-versions) }}
+ env:
+ PYTHON_MAJOR_MINOR_VERSION: ${{ matrix.python-version }}
+ GITHUB_REPOSITORY: ${{ github.repository }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GITHUB_USERNAME: ${{ github.actor }}
+ steps:
+ - name: "Cleanup repo"
+ shell: bash
+ run: sudo rm -rf ${GITHUB_WORKSPACE}/*
+ - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+ - name: "Prepare breeze & PROD image: ${{ env.PYTHON_MAJOR_MINOR_VERSION }}"
+ uses: ./.github/actions/prepare_breeze_and_image
+ with:
+ platform: "linux/amd64"
+ image-type: "prod"
+ python: ${{ env.PYTHON_MAJOR_MINOR_VERSION }}
+ use-uv: ${{ needs.build-info.outputs.use-uv }}
+ make-mnt-writeable-and-cleanup: true
+ # Guards against the canary silently testing the default flavor if the flavor or Debian version
+ # stop being passed through to the build.
+ - name: "Verify the image is the legacy bookworm flavor"
+ env:
+ DEFAULT_BRANCH: ${{ needs.build-info.outputs.default-branch }}
+ run: |
+ image="ghcr.io/${GITHUB_REPOSITORY,,}/${DEFAULT_BRANCH}/prod/python${PYTHON_MAJOR_MINOR_VERSION}"
+ flavor=$(docker inspect "${image}" \
+ --format '{{ index .Config.Labels "org.apache.airflow.image.flavor" }}')
+ codename=$(docker run --rm --entrypoint /bin/bash "${image}" \
+ -c '. /etc/os-release && echo "${VERSION_CODENAME}"')
+ echo "Image flavor: ${flavor}, Debian: ${codename}"
+ [[ "${flavor}" == "legacy" && "${codename}" == "bookworm" ]]
+
+ additional-prod-image-tests:
+ name: "PROD image tests"
+ needs: [build-info, build-prod-images, constraints]
+ uses: ./.github/workflows/additional-prod-image-tests.yml
+ with:
+ runners: '["ubuntu-22.04"]'
+ platform: "linux/amd64"
+ default-branch: ${{ needs.build-info.outputs.default-branch }}
+ constraints-branch: ${{ needs.build-info.outputs.default-constraints-branch }}
+ upgrade-to-newer-dependencies: "false"
+ docker-cache: "disabled"
+ disable-airflow-repo-cache: ${{ needs.build-info.outputs.disable-airflow-repo-cache }}
+ default-python-version: "${{ needs.build-info.outputs.default-python-version }}"
+ run-task-sdk-integration-tests: ${{ needs.build-info.outputs.run-task-sdk-integration-tests }}
+ canary-run: ${{ needs.build-info.outputs.canary-run }}
+ run-remote-logging-elasticsearch-e2e-tests: >-
+ ${{ needs.build-info.outputs.run-remote-logging-elasticsearch-e2e-tests }}
+ run-remote-logging-opensearch-e2e-tests: >-
+ ${{ needs.build-info.outputs.run-remote-logging-opensearch-e2e-tests }}
+ run-remote-logging-s3-e2e-tests: ${{ needs.build-info.outputs.run-remote-logging-s3-e2e-tests }}
+ run-event-driven-e2e-tests: ${{ needs.build-info.outputs.run-event-driven-e2e-tests }}
+ run-java-sdk-e2e-tests: ${{ needs.build-info.outputs.run-java-sdk-e2e-tests }}
+ java-sdk-version: ${{ needs.build-info.outputs.java-sdk-version }}
+ run-go-sdk-e2e-tests: ${{ needs.build-info.outputs.run-go-sdk-e2e-tests }}
+ run-openlineage-e2e-tests: ${{ needs.build-info.outputs.run-openlineage-e2e-tests }}
+ run-openlineage-e2e-compat-tests: ${{ needs.build-info.outputs.run-openlineage-e2e-compat-tests }}
+ providers-compatibility-tests-matrix: >
+ ${{ needs.build-info.outputs.providers-compatibility-tests-matrix }}
+ run-ts-sdk-e2e-tests: ${{ needs.build-info.outputs.run-ts-sdk-e2e-tests }}
+ use-uv: ${{ needs.build-info.outputs.use-uv }}
+ run-ui-e2e-tests: ${{ needs.build-info.outputs.run-ui-e2e-tests }}
+ run-airflow-ctl-integration-tests: ${{ needs.build-info.outputs.run-airflow-ctl-integration-tests }}
+
+ tests-kubernetes:
+ name: "Kubernetes tests"
+ needs: [build-info, build-prod-images]
+ uses: ./.github/workflows/k8s-tests.yml
+ permissions:
+ contents: read
+ packages: read
+ with:
+ runners: '["ubuntu-22.04"]'
+ platform: "linux/amd64"
+ python-versions-list-as-string: ${{ needs.build-info.outputs.python-versions-list-as-string }}
+ include-success-outputs: ${{ needs.build-info.outputs.include-success-outputs }}
+ use-uv: ${{ needs.build-info.outputs.use-uv }}
+ debug-resources: ${{ needs.build-info.outputs.debug-resources }}
+ kubernetes-combos: ${{ needs.build-info.outputs.kubernetes-combos }}
+ java-sdk-version: ${{ needs.build-info.outputs.java-sdk-version }}
+ # yamllint disable rule:line-length
+ lang-sdk-kubernetes-combo: >-
+ ${{ needs.build-info.outputs.default-python-version }}-${{ needs.build-info.outputs.default-kubernetes-version }}
+ # yamllint enable rule:line-length
diff --git a/Dockerfile b/Dockerfile
index 8d31ff9c9d0..94bd2533a82 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -50,23 +50,22 @@ ARG AIRFLOW_USER_HOME_DIR=/home/airflow
# latest released version here
ARG AIRFLOW_VERSION="3.3.2"
-ARG BASE_IMAGE="debian:bookworm-slim"
-ARG AIRFLOW_PYTHON_VERSION="3.13.16"
-
-# PYTHON_LTO: Controls whether Python is built with Link-Time Optimization (LTO).
-#
-# Link-Time Optimization uses MD5 checksums during the compilation process to verify
-# object files and intermediate representations. In FIPS-compliant environments, MD5
-# is blocked as it's not an approved cryptographic algorithm (see FIPS 140-2/140-3).
-# This can cause Python builds with LTO to fail when FIPS mode is enabled.
+# BASE_IMAGE: the Docker Hardened Image that Python comes from. Both the build stage and the final
+# image are based on it, so Python is never compiled during the build.
#
-# When building FIPS-compliant images, set this to "false" to disable LTO:
-# docker build --build-arg PYTHON_LTO="false" ...
+# The default is Airflow's public mirror of the upstream "dhi.io/python" image, so building needs no
+# registry credentials. The upstream image itself requires a "docker login dhi.io".
#
-# Default: "true" (LTO enabled for better performance)
+# The FIPS-validated variants require a paid Docker subscription and cannot be mirrored or made the
+# default, but you can build a FIPS-compliant image by pointing the build at one:
+# docker build . --build-arg BASE_IMAGE="dhi.io/python:3.13.16-debian13-fips-dev"
#
-# Related: https://github.com/apache/airflow/issues/58337
-ARG PYTHON_LTO="true"
+# AIRFLOW_IMAGE_FLAVOR="legacy" builds the image the way it was built before the hardened base images:
+# BASE_IMAGE is then a plain Debian image (for example "debian:bookworm-slim") and Python
+# AIRFLOW_PYTHON_VERSION is downloaded from python.org, verified and compiled from sources.
+ARG AIRFLOW_IMAGE_FLAVOR="hardened"
+ARG BASE_IMAGE="ghcr.io/apache/airflow/base/python:3.13.16-debian13-dev"
+ARG AIRFLOW_PYTHON_VERSION="3.13.16"
# You can swap comments between those two args to test pip from the main version
# When you attempt to test if the version of `pip` from specified branch works for our builds
@@ -119,15 +118,29 @@ if [[ "$#" != 1 ]]; then
exit 1
fi
-AIRFLOW_PYTHON_VERSION=${AIRFLOW_PYTHON_VERSION:-3.11.16}
-PYTHON_LTO=${PYTHON_LTO:-true}
GOLANG_MAJOR_MINOR_VERSION=${GOLANG_MAJOR_MINOR_VERSION:-1.24.4}
TEMURIN_VERSION=${TEMURIN_VERSION:-11}
NODEJS_VERSION=${NODEJS_VERSION:-22.23.1}
PNPM_VERSION=${PNPM_VERSION:-10.28.1}
RUSTUP_DEFAULT_TOOLCHAIN=${RUSTUP_DEFAULT_TOOLCHAIN:-stable}
RUSTUP_VERSION=${RUSTUP_VERSION:-1.29.0}
+AIRFLOW_IMAGE_FLAVOR=${AIRFLOW_IMAGE_FLAVOR:-hardened}
+AIRFLOW_PYTHON_VERSION=${AIRFLOW_PYTHON_VERSION:-3.13.16}
COSIGN_VERSION=${COSIGN_VERSION:-3.0.5}
+if [[ "${AIRFLOW_IMAGE_FLAVOR}" == "hardened" ]]; then
+ # The Debian 12 hardened images ship Python under /opt/python, the Debian 13 ones install it as
+ # Debian packages under /usr - so ask the base image's Python where it lives.
+ PYTHON_HOME=${PYTHON_HOME:-$(python3 -c 'import sys; print(sys.base_prefix)')}
+ # Read before apt runs: a package depending on python3 can pull another Python version in.
+ BASE_PYTHON_MAJOR_MINOR=$(python3 -c 'import sys; print(f"{sys.version_info[0]}.{sys.version_info[1]}")')
+elif [[ "${AIRFLOW_IMAGE_FLAVOR}" == "legacy" ]]; then
+ PYTHON_HOME=${PYTHON_HOME:-/usr/python}
+else
+ echo
+ echo "ERROR! AIRFLOW_IMAGE_FLAVOR should be 'hardened' or 'legacy', not '${AIRFLOW_IMAGE_FLAVOR}'."
+ echo
+ exit 1
+fi
if [[ "${1}" == "runtime" ]]; then
INSTALLATION_TYPE="RUNTIME"
@@ -154,38 +167,25 @@ freetds-dev \
git \
graphviz \
graphviz-dev \
+gzip \
krb5-user \
-lcov \
ldap-utils \
-libbluetooth-dev \
-libbz2-dev \
libc6-dev \
-libdb-dev \
libev-dev \
libev4 \
libffi-dev \
-libgdbm-compat-dev \
-libgdbm-dev \
libgeos-dev \
libkrb5-dev \
libldap2-dev \
libleveldb-dev \
libleveldb1d \
-liblzma-dev \
-libncurses5-dev \
-libreadline6-dev \
libsasl2-2 \
libsasl2-dev \
libsasl2-modules \
-libsqlite3-dev \
libssl-dev \
libxmlsec1 \
libxmlsec1-dev \
-libzstd-dev \
locales \
-lsb-release \
-lzma \
-lzma-dev \
openssh-client \
openssl \
pkg-config \
@@ -194,14 +194,26 @@ sasl2-bin \
sqlite3 \
sudo \
tdsodbc \
-tk-dev \
unixodbc \
unixodbc-dev \
-uuid-dev \
wget \
xz-utils \
zlib1g-dev \
"
+ if [[ "${AIRFLOW_IMAGE_FLAVOR}" == "legacy" ]]; then
+ # Needed only to compile Python from sources - without them it builds, but silently
+ # lacks modules such as sqlite3, lzma or readline.
+ local debian_version
+ # shellcheck disable=SC1091
+ debian_version=$(. /etc/os-release; printf '%s\n' "$VERSION_CODENAME")
+ if [[ "${debian_version}" == "bookworm" ]]; then
+ DEV_APT_DEPS+="lcov libbluetooth-dev libbz2-dev libdb-dev libgdbm-compat-dev libgdbm-dev \
+liblzma-dev libncurses5-dev libreadline6-dev libsqlite3-dev libzstd-dev lzma lzma-dev tk-dev uuid-dev"
+ else
+ DEV_APT_DEPS+="lcov libbluetooth-dev libbz2-dev libdb-dev libgdbm-compat-dev libgdbm-dev \
+liblzma-dev libncurses-dev libreadline-dev libsqlite3-dev libzstd-dev tk-dev uuid-dev"
+ fi
+ fi
export DEV_APT_DEPS
fi
}
@@ -215,15 +227,29 @@ function get_runtime_apt_deps() {
echo
echo "DEBIAN CODENAME: ${debian_version}"
echo
- debian_version_apt_deps="\
+ if [[ "${debian_version}" == "bookworm" ]]; then
+ debian_version_apt_deps="\
libffi8 \
libldap-2.5-0 \
libssl3 \
netcat-openbsd\
"
+ else
+ # trixie renamed the libraries that moved to a 64-bit time_t (libssl3 -> libssl3t64) and
+ # dropped the soname from the LDAP library package name.
+ debian_version_apt_deps="\
+libffi8 \
+libldap2 \
+libssl3t64 \
+netcat-openbsd\
+"
+ fi
echo
echo "APPLIED INSTALLATION CONFIGURATION FOR DEBIAN VERSION: ${debian_version}"
echo
+ # libxmlsec1-openssl was added because libxmlsec1 ships no crypto engine of its own - the engines
+ # are separate packages - so the "xmlsec" module (pulled in by python3-saml) imported with
+ # "libxmlsec1-openssl.so.1: cannot open shared object file" without it.
if [[ "${RUNTIME_APT_DEPS=}" == "" ]]; then
RUNTIME_APT_DEPS="\
${debian_version_apt_deps} \
@@ -242,8 +268,8 @@ libgeos-dev \
libsasl2-2 \
libsasl2-modules \
libxmlsec1 \
+libxmlsec1-openssl \
locales \
-lsb-release \
openssh-client \
rsync \
sasl2-bin \
@@ -272,10 +298,82 @@ function install_docker_cli() {
apt-get install -y --no-install-recommends docker-ce-cli
}
+function keep_image_conffiles() {
+ # The hardened base images ship a modified /etc/debian_version, so any apt run that upgrades
+ # base-files stops at dpkg's interactive conffile prompt and fails a non-interactive build. Making
+ # dpkg keep the image's copy by default covers every apt run - this build's and those of images
+ # extending it - so no install command has to remember the flags.
+ mkdir -p /etc/dpkg/dpkg.cfg.d
+ printf '%s\n' force-confdef force-confold > /etc/dpkg/dpkg.cfg.d/airflow-keep-conffiles
+}
+
+function refresh_ldconfig_after_dpkg() {
+ # The hardened base images ship libc-bin without its dpkg trigger, so installing a shared library
+ # does not refresh the ldconfig cache and ctypes.util.find_library() - which reads that cache -
+ # cannot find it (for example pyenchant's "The 'enchant' C library was not found"). The hook covers
+ # every apt run - this build's and those of images extending it.
+ mkdir -p /etc/apt/apt.conf.d
+ echo 'DPkg::Post-Invoke { "if [ -x /sbin/ldconfig ]; then /sbin/ldconfig; fi"; };' \
+ > /etc/apt/apt.conf.d/99airflow-ldconfig
+}
+
+function restore_debian_base_files() {
+ # The hardened base images ship a minimal /etc, but Debian maintainer scripts assume the files
+ # a stock Debian has: sasl2-bin chowns its run directory to the "sasl" group from base-passwd,
+ # and tmux registers its shell with add-shell, which reads /etc/shells. The base-passwd package
+ # only ships the reference copies of the account files - update-passwd is what merges them
+ # into /etc.
+ # libpam-runtime generates the /etc/pam.d/common-* files that the PAM configs already in the
+ # image "@include" - without them "adduser --gecos" aborts with a PAM error from chfn.
+ # Debian Policy lets a maintainer script rely on any essential package without declaring it, and
+ # libgcrypt20 takes that up: its postinst runs a helper with a "#!/bin/dash" shebang. The hardened
+ # images ship /bin/sh as a symlink to bash and no dash at all, so configuring the package aborts
+ # with exit 127 and takes the whole apt transaction with it. Installing it first restores the
+ # assumption, and hands /bin/sh back to dash the way a stock Debian has it.
+ apt-get install -y --no-install-recommends dash
+ apt-get install -y --no-install-recommends base-passwd libpam-runtime
+ update-passwd
+ # The Debian 13 hardened images strip /etc/pam.d further: libpam-runtime and passwd are already
+ # installed there, so installing them generates nothing, and passwd, chpasswd and chfn abort with
+ # "pam_start() failed". Reinstalling passwd brings back its deleted PAM configs, and
+ # pam-auth-update - which treats the deleted common-* files as local changes - needs --force.
+ apt-get install -y --no-install-recommends --reinstall -o Dpkg::Options::=--force-confmiss passwd
+ if [[ ! -e /etc/pam.d/common-auth ]]; then
+ pam-auth-update --package --force
+ fi
+ if [[ ! -e /etc/shells ]]; then
+ printf '%s\n' "# /etc/shells: valid login shells" /bin/sh /bin/bash > /etc/shells
+ fi
+ align_perl_base_with_perl
+}
+
+function align_perl_base_with_perl() {
+ # The hardened images ship their own build of perl-base, but the hardened apt repository does not
+ # always publish the matching perl - while it still publishes packages that depend on perl, such
+ # as git. Debian's perl depends on exactly its own perl-base version, so apt refuses to install
+ # git at all. Moving perl-base to the version of the perl apt can install keeps the two in step;
+ # when the repository does publish a matching perl, the versions already agree and this does nothing.
+ local perl_version
+ local perl_base_version
+ perl_version=$(apt-cache policy perl | awk '/Candidate:/ {print $2}')
+ perl_base_version=$(dpkg-query -W -f='${Version}' perl-base 2>/dev/null || true)
+ if [[ -n "${perl_version}" && "${perl_version}" != "(none)" && -n "${perl_base_version}" \
+ && "${perl_base_version}" != "${perl_version}" ]]; then
+ apt-get install -y --no-install-recommends --allow-downgrades "perl-base=${perl_version}"
+ fi
+}
+
function install_debian_dev_dependencies() {
+ if [[ "${AIRFLOW_IMAGE_FLAVOR}" == "hardened" ]]; then
+ keep_image_conffiles
+ refresh_ldconfig_after_dpkg
+ fi
apt-get update
apt-get install -yqq --no-install-recommends apt-utils >/dev/null 2>&1
- apt-get install -y --no-install-recommends wget curl gnupg2 lsb-release ca-certificates
+ if [[ "${AIRFLOW_IMAGE_FLAVOR}" == "hardened" ]]; then
+ restore_debian_base_files
+ fi
+ apt-get install -y --no-install-recommends wget curl gnupg2 ca-certificates
# shellcheck disable=SC2086
export ${ADDITIONAL_DEV_APT_ENV?}
if [[ ${DEV_APT_COMMAND} != "" ]]; then
@@ -305,14 +403,25 @@ function install_additional_dev_dependencies() {
}
function link_python() {
+ # Airflow images have always exposed Python under /usr/python - documentation, volume mounts and
+ # user customizations refer to that path - while the hardened base images ship it in /opt/python
+ # (Debian 12) or /usr (Debian 13), so keep the historical location working as a symlink.
+ if [[ ! -e /usr/python ]]; then
+ ln -sv "${PYTHON_HOME}" /usr/python
+ fi
+ # The hardened base images have no /usr/local tree at all
+ mkdir -p /usr/local/bin /usr/local/lib
# link python binaries to /usr/local/bin and /usr/python/bin with and without 3 suffix
# Links in /usr/local/bin are needed for tools that expect python to be there
# Links in /usr/python/bin are needed for tools that are detecting home of python installation including
# lib/site-packages. The /usr/python/bin should be first in PATH in order to help with the last part.
for dst in pip3 python3 python3-config; do
src="$(echo "${dst}" | tr -d 3)"
+ if [[ ! -e "/usr/python/bin/${dst}" ]]; then
+ continue
+ fi
echo "Linking ${dst} in /usr/local/bin and /usr/python/bin"
- ln -sv "/usr/python/bin/${dst}" "/usr/local/bin/${dst}"
+ ln -sfv "/usr/python/bin/${dst}" "/usr/local/bin/${dst}"
for dir in /usr/local/bin /usr/python/bin; do
if [[ ! -e "${dir}/${src}" ]]; then
echo "Creating ${src} - > ${dst} link in ${dir}"
@@ -320,37 +429,103 @@ function link_python() {
fi
done
done
- for dst in /usr/python/lib/*
- do
- src="/usr/local/lib/$(basename "${dst}")"
- if [[ -e "${src}" ]]; then
- rm -rf "${src}"
- fi
- echo "Linking ${dst} to ${src}"
- ln -sv "${dst}" "${src}"
- done
+ # A Python installed under /usr already has its libraries where the dynamic linker looks, and
+ # linking all of /usr/lib into /usr/local/lib would only shadow the system libraries.
+ if [[ "$(readlink -f "${PYTHON_HOME}")" != "/usr" ]]; then
+ for dst in /usr/python/lib/*
+ do
+ src="/usr/local/lib/$(basename "${dst}")"
+ if [[ -e "${src}" ]]; then
+ rm -rf "${src}"
+ fi
+ echo "Linking ${dst} to ${src}"
+ ln -sv "${dst}" "${src}"
+ done
+ fi
ldconfig
}
-function install_debian_runtime_dependencies() {
- apt-get update
- apt-get install --no-install-recommends -yqq apt-utils >/dev/null 2>&1
- apt-get install -y --no-install-recommends wget curl gnupg2 lsb-release ca-certificates
- # shellcheck disable=SC2086
- export ${ADDITIONAL_RUNTIME_APT_ENV?}
- if [[ "${RUNTIME_APT_COMMAND}" != "" ]]; then
- bash -o pipefail -o errexit -o nounset -o nolog -c "${RUNTIME_APT_COMMAND}"
+function restore_thread_stack_size() {
+ # The hardened base images build Python with -DTHREAD_STACK_SIZE=0x100000, so every thread Python
+ # starts gets a 1 MiB stack instead of glibc's default (the 8 MiB "ulimit -s" the previous images
+ # used). On Python 3.12 and 3.13 the C recursion guard is a fixed depth count sized for the larger
+ # stack, so deeply nested input - e.g. a JSON body parsed in a web server worker thread - overflows
+ # the stack and kills the process instead of raising RecursionError. A .pth file runs at every
+ # interpreter start-up without taking the sitecustomize module name users may already rely on.
+ local site_packages
+ site_packages="$(/usr/python/bin/python -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')"
+ echo "Restoring the 8 MiB default thread stack size in ${site_packages}"
+ echo "import threading; threading.stack_size(8 * 1024 * 1024)" > "${site_packages}/airflow-thread-stack-size.pth"
+}
+
+function compile_python_stdlib() {
+ # The hardened base images ship the standard library with no .pyc files at all. Python then misses
+ # the bytecode cache on every stdlib import, and in a read-only or non-writable directory it cannot
+ # create one - each miss leaves a negative dentry in the kernel, which grows without bound in a
+ # long-running container and can exhaust memory on the host. Compiling the standard library here
+ # restores what the previously compiled-in-image Python shipped.
+ # See https://github.com/apache/airflow/pull/58944 and https://lwn.net/Articles/814535/
+ local stdlib
+ stdlib="$(/usr/python/bin/python -c 'import sysconfig; print(sysconfig.get_paths()["stdlib"])')"
+ echo "Compiling Python standard library in ${stdlib}"
+ # compileall exits non-zero when any file fails to compile, and the standard library ships files
+ # that are meant not to compile (deliberately broken syntax used by the test suite).
+ /usr/python/bin/python -m compileall -q -j "$(nproc)" -o 0 -o 1 -o 2 "${stdlib}" || true
+}
+
+function hold_hardened_python() {
+ # The Debian 13 hardened images install their Python as packages that conflict with Debian's own
+ # python3. A package depending on python3 - for example NodeSource's nodejs - therefore makes apt
+ # remove the image's Python and install Debian's, without asking. Holding the packages turns that
+ # into an apt error instead, in this build and in images extending it.
+ local packages
+ packages=$(dpkg-query -W -f='${db:Status-Abbrev} ${Package}\n' 'python-3.*' 'libpython-3.*' 2>/dev/null \
+ | awk '$1 == "ii" {print $2}' || true)
+ if [[ -n "${packages}" ]]; then
+ # shellcheck disable=SC2086
+ apt-mark hold ${packages}
fi
- if [[ "${ADDITIONAL_RUNTIME_APT_COMMAND}" != "" ]]; then
- bash -o pipefail -o errexit -o nounset -o nolog -c "${ADDITIONAL_RUNTIME_APT_COMMAND}"
+}
+
+function check_no_system_python() {
+ # Python - from the hardened base image or compiled from sources - must stay the only Python in the
+ # image. A system Python pulled in as a dependency of an apt package shares its shared libraries
+ # with ours and leads to errors such as:
+ # /usr/python/lib/python3.11/lib-dynload/_ssl.cpython-311-aarch64-linux-gnu.so: undefined symbol: _PyModule_Add
+ # Debian names its Python libraries "libpython3.13". The trixie hardened images register their own
+ # Python with dpkg as "libpython-3.13", which is the Python we want, so it must not match.
+ # Debian 13 hardened images package their Python as "python-3.13" and serve other versions from
+ # their apt repository, so a package depending on python3 can also pull in a hardened Python of a
+ # different version than the image's own.
+ local other_hardened_python=""
+ if [[ -n "${BASE_PYTHON_MAJOR_MINOR=}" ]]; then
+ other_hardened_python=$(dpkg -l | awk '/^ii (lib)?python-3\.[0-9]+/ {print $2}' \
+ | grep -v -E "python-${BASE_PYTHON_MAJOR_MINOR//./\\.}(-|:|$)" || true)
+ fi
+ if [[ -n "${other_hardened_python}" ]]; then
+ echo
+ echo "ERROR! A Python other than the image's ${BASE_PYTHON_MAJOR_MINOR} was installed: ${other_hardened_python//$'\n'/ }"
+ echo
+ apt-get install -yqq aptitude >/dev/null
+ aptitude why "$(echo "${other_hardened_python}" | head -1 | cut -d: -f1)"
+ echo
+ exit 1
+ fi
+ if dpkg -l | grep -E '^ii libpython3\.[0-9]+' >/dev/null; then
+ echo
+ echo "ERROR! System python is installed by one of the previous steps"
+ echo
+ echo "Please make sure that no python packages are installed by default. Displaying the reason why libpython is installed:"
+ echo
+ apt-get install -yqq aptitude >/dev/null
+ aptitude why "$(dpkg -l | grep -E '^ii libpython3\.[0-9]+' | head -1 | awk '{print $2}')"
+ echo
+ exit 1
+ else
+ echo
+ echo "GOOD! System python is not installed - OK"
+ echo
fi
- apt-get update
- # shellcheck disable=SC2086
- apt-get install -y --no-install-recommends ${RUNTIME_APT_DEPS} ${ADDITIONAL_RUNTIME_APT_DEPS}
- apt-get autoremove -yqq --purge
- apt-get clean
- link_python
- rm -rf /var/lib/apt/lists/* /var/log/*
}
function install_cosign() {
@@ -374,27 +549,7 @@ function install_cosign() {
}
function install_python() {
- # If system python (3.11 in bookworm) is installed (via automatic installation of some dependencies for example), we need
- # to fail and make sure that it is not there, because there can be strange interactions if we install
- # newer version and system libraries are installed, because
- # when you create a virtualenv part of the shared libraries of Python can be taken from the system
- # Installation leading to weird errors when you want to install some modules - for example when you install ssl:
- # /usr/python/lib/python3.11/lib-dynload/_ssl.cpython-311-aarch64-linux-gnu.so: undefined symbol: _PyModule_Add
- if dpkg -l | grep '^ii' | grep '^ii libpython' >/dev/null; then
- echo
- echo "ERROR! System python is installed by one of the previous steps"
- echo
- echo "Please make sure that no python packages are installed by default. Displaying the reason why libpython3.11 is installed:"
- echo
- apt-get install -yqq aptitude >/dev/null
- aptitude why libpython3.11
- echo
- exit 1
- else
- echo
- echo "GOOD! System python is not installed - OK"
- echo
- fi
+ # Only the legacy image flavor compiles Python - the hardened flavor gets it from the base image.
wget --tries=3 --waitretry=5 -O python.tar.xz "https://www.python.org/ftp/python/${AIRFLOW_PYTHON_VERSION%%[a-z]*}/Python-${AIRFLOW_PYTHON_VERSION}.tar.xz"
local major_minor_version
major_minor_version="${AIRFLOW_PYTHON_VERSION%.*}"
@@ -440,21 +595,13 @@ function install_python() {
EXTRA_CFLAGS="${EXTRA_CFLAGS:-} -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer";
LDFLAGS="$(dpkg-buildflags --get LDFLAGS)"
LDFLAGS="${LDFLAGS:--Wl},--strip-all"
- # Link-Time Optimization (LTO) uses MD5 checksums for object file verification during
- # compilation. In FIPS mode, MD5 is blocked as a non-approved algorithm, causing builds
- # to fail. The PYTHON_LTO variable allows disabling LTO for FIPS-compliant builds.
- # See: https://github.com/apache/airflow/issues/58337
- local lto_option=""
- if [[ "${PYTHON_LTO:-true}" == "true" ]]; then
- lto_option="--with-lto"
- fi
local build_log
build_log=$(mktemp)
echo "Building Python ${AIRFLOW_PYTHON_VERSION} from source..."
if ! (
./configure --enable-optimizations --prefix=/usr/python/ --with-ensurepip --build="$gnuArch" \
--enable-loadable-sqlite-extensions --enable-option-checking=fatal \
- --enable-shared ${lto_option} && \
+ --enable-shared --with-lto && \
make -s -j "$(nproc)" "EXTRA_CFLAGS=${EXTRA_CFLAGS:-}" \
"LDFLAGS=${LDFLAGS:--Wl},-rpath='\$\$ORIGIN/../lib'" python && \
make -s -j "$(nproc)" install
@@ -467,6 +614,9 @@ function install_python() {
exit 1
fi
rm -f "${build_log}"
+ # CPython builds without the optional modules whose -dev packages are missing, so check them here
+ # rather than finding out from a broken image.
+ /usr/python/bin/python3 -c "import bz2, ctypes, dbm.gnu, lzma, readline, sqlite3, ssl, uuid, zlib"
cd /
rm -rf /usr/src/python
find /usr/python -depth \
@@ -477,6 +627,40 @@ function install_python() {
link_python
}
+function install_debian_runtime_dependencies() {
+ if [[ "${AIRFLOW_IMAGE_FLAVOR}" == "hardened" ]]; then
+ keep_image_conffiles
+ refresh_ldconfig_after_dpkg
+ fi
+ apt-get update
+ apt-get install --no-install-recommends -yqq apt-utils >/dev/null 2>&1
+ if [[ "${AIRFLOW_IMAGE_FLAVOR}" == "hardened" ]]; then
+ restore_debian_base_files
+ fi
+ apt-get install -y --no-install-recommends wget curl gnupg2 ca-certificates
+ # shellcheck disable=SC2086
+ export ${ADDITIONAL_RUNTIME_APT_ENV?}
+ if [[ "${RUNTIME_APT_COMMAND}" != "" ]]; then
+ bash -o pipefail -o errexit -o nounset -o nolog -c "${RUNTIME_APT_COMMAND}"
+ fi
+ if [[ "${ADDITIONAL_RUNTIME_APT_COMMAND}" != "" ]]; then
+ bash -o pipefail -o errexit -o nounset -o nolog -c "${ADDITIONAL_RUNTIME_APT_COMMAND}"
+ fi
+ apt-get update
+ # shellcheck disable=SC2086
+ apt-get install -y --no-install-recommends ${RUNTIME_APT_DEPS} ${ADDITIONAL_RUNTIME_APT_DEPS}
+ apt-get autoremove -yqq --purge
+ apt-get clean
+ check_no_system_python
+ link_python
+ if [[ "${AIRFLOW_IMAGE_FLAVOR}" == "hardened" ]]; then
+ hold_hardened_python
+ restore_thread_stack_size
+ compile_python_stdlib
+ fi
+ rm -rf /var/lib/apt/lists/* /var/log/*
+}
+
function install_golang() {
curl --retry 3 --retry-delay 5 "https://dl.google.com/go/go${GOLANG_MAJOR_MINOR_VERSION}.linux-$(dpkg --print-architecture).tar.gz" -o "go${GOLANG_MAJOR_MINOR_VERSION}.linux.tar.gz"
rm -rf /usr/local/go && tar -C /usr/local -xzf go"${GOLANG_MAJOR_MINOR_VERSION}".linux.tar.gz
@@ -573,7 +757,15 @@ if [[ "${INSTALLATION_TYPE}" == "RUNTIME" ]]; then
else
get_dev_apt_deps
install_debian_dev_dependencies
- install_python
+ check_no_system_python
+ if [[ "${AIRFLOW_IMAGE_FLAVOR}" == "hardened" ]]; then
+ link_python
+ hold_hardened_python
+ restore_thread_stack_size
+ compile_python_stdlib
+ else
+ install_python
+ fi
install_additional_dev_dependencies
install_rustup
if [[ "${INSTALLATION_TYPE}" == "CI" ]]; then
@@ -596,7 +788,6 @@ set -euo pipefail
common::get_colors
declare -a packages
-readonly MARIADB_LTS_VERSION="10.11"
: "${INSTALL_MYSQL_CLIENT:?Should be true or false}"
: "${INSTALL_MYSQL_CLIENT_TYPE:-mariadb}"
@@ -650,6 +841,13 @@ retry() {
}
install_mariadb_client() {
+ # https://mariadb.org/about/#maintenance-policy
+ # MariaDB publishes each LTS series only for the Debian releases current when it came out - 10.11
+ # has no trixie repository and 11.8 is the first LTS that does.
+ local mariadb_lts_version="10.11"
+ if [[ "$(common::debian_codename)" != "bookworm" ]]; then
+ mariadb_lts_version="11.8"
+ fi
# List of compatible package Oracle MySQL -> MariaDB:
# `mysql-client` -> `mariadb-client` or `mariadb-client-compat` (11+)
# `libmysqlclientXX` (where XX is a number) -> `libmariadb3-compat`
@@ -669,15 +867,19 @@ install_mariadb_client() {
echo
exit 1
fi
+ if [[ "${mariadb_lts_version%%.*}" -ge 11 ]]; then
+ # From 11.0 mariadb-client no longer ships the "mysql" named commands
+ packages+=("mariadb-client-compat")
+ fi
common::import_trusted_gpg "0xF1656F24C74CD1D8" "mariadb"
echo
- echo "${COLOR_BLUE}Installing MariaDB client version ${MARIADB_LTS_VERSION}: ${1}${COLOR_RESET}"
+ echo "${COLOR_BLUE}Installing MariaDB client version ${mariadb_lts_version}: ${1}${COLOR_RESET}"
echo "${COLOR_YELLOW}MariaDB client protocol-compatible with MySQL client.${COLOR_RESET}"
echo
- echo "deb [arch=amd64,arm64] https://archive.mariadb.org/mariadb-${MARIADB_LTS_VERSION}/repo/debian/ $(lsb_release -cs) main" > \
+ echo "deb [arch=amd64,arm64] https://archive.mariadb.org/mariadb-${mariadb_lts_version}/repo/debian/ $(common::debian_codename) main" > \
/etc/apt/sources.list.d/mariadb.list
# Make sure that dependencies from MariaDB repo are preferred over Debian dependencies
printf "Package: *\nPin: release o=MariaDB\nPin-Priority: 999\n" > /etc/apt/preferences.d/mariadb
@@ -716,12 +918,14 @@ function install_mssql_client() {
packages=("msodbcsql18")
common::import_trusted_gpg "EB3E94ADBE1229CF" "microsoft"
+ # The Debian 13 repository is signed with Microsoft's newer key, the older releases with the one above.
+ common::import_trusted_gpg "EE4D7792F748182B" "microsoft-2025"
echo
echo "${COLOR_BLUE}Installing mssql client${COLOR_RESET}"
echo
- echo "deb [arch=amd64,arm64] https://packages.microsoft.com/debian/$(lsb_release -rs)/prod $(lsb_release -cs) main" > \
+ echo "deb [arch=amd64,arm64] https://packages.microsoft.com/debian/$(common::debian_release)/prod $(common::debian_codename) main" > \
/etc/apt/sources.list.d/mssql-release.list &&
mkdir -p /opt/microsoft/msodbcsql18 &&
touch /opt/microsoft/msodbcsql18/ACCEPT_EULA &&
@@ -765,7 +969,7 @@ install_postgres_client() {
common::import_trusted_gpg "7FCC7D46ACCC4CF8" "postgres"
- echo "deb [arch=amd64,arm64] https://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > \
+ echo "deb [arch=amd64,arm64] https://apt.postgresql.org/pub/repos/apt/ $(common::debian_codename)-pgdg main" > \
/etc/apt/sources.list.d/pgdg.list
apt-get update
apt-get install --no-install-recommends -y "${packages[@]}"
@@ -912,12 +1116,12 @@ function common::get_constraints_location() {
function common::show_packaging_tool_version_and_location() {
echo "PATH=${PATH}"
- echo "Installed pip: $(pip --version): $(which pip)"
+ echo "Installed pip: $(pip --version): $(command -v pip)"
if [[ ${PACKAGING_TOOL} == "pip" ]]; then
echo "${COLOR_BLUE}Using 'pip' to install Airflow${COLOR_RESET}"
else
echo "${COLOR_BLUE}Using 'uv' to install Airflow${COLOR_RESET}"
- echo "Installed uv: $(uv --version 2>/dev/null || echo "Not installed yet"): $(which uv 2>/dev/null)"
+ echo "Installed uv: $(uv --version 2>/dev/null || echo "Not installed yet"): $(command -v uv 2>/dev/null)"
fi
}
@@ -929,7 +1133,7 @@ function common::install_packaging_tools() {
echo
else
echo
- echo "${COLOR_BLUE}Checking packaging tools for system Python installation: $(which python)${COLOR_RESET}"
+ echo "${COLOR_BLUE}Checking packaging tools for system Python installation: $(command -v python)${COLOR_RESET}"
echo
fi
if [[ ${AIRFLOW_PIP_VERSION=} == "" ]]; then
@@ -993,6 +1197,16 @@ function common::install_packaging_tools() {
fi
}
+function common::debian_codename() {
+ # shellcheck disable=SC1091
+ (. /etc/os-release; printf '%s\n' "${VERSION_CODENAME}")
+}
+
+function common::debian_release() {
+ # shellcheck disable=SC1091
+ (. /etc/os-release; printf '%s\n' "${VERSION_ID}")
+}
+
function common::import_trusted_gpg() {
common::get_colors
@@ -1887,23 +2101,30 @@ ARG DEV_APT_COMMAND=""
ARG ADDITIONAL_DEV_APT_COMMAND=""
ARG ADDITIONAL_DEV_APT_ENV=""
ARG AIRFLOW_PYTHON_VERSION
+ARG AIRFLOW_IMAGE_FLAVOR
ENV DEV_APT_DEPS=${DEV_APT_DEPS} \
ADDITIONAL_DEV_APT_DEPS=${ADDITIONAL_DEV_APT_DEPS} \
DEV_APT_COMMAND=${DEV_APT_COMMAND} \
ADDITIONAL_DEV_APT_COMMAND=${ADDITIONAL_DEV_APT_COMMAND} \
ADDITIONAL_DEV_APT_ENV=${ADDITIONAL_DEV_APT_ENV} \
- AIRFLOW_PYTHON_VERSION=${AIRFLOW_PYTHON_VERSION}
-
-ARG PYTHON_LTO
+ AIRFLOW_PYTHON_VERSION=${AIRFLOW_PYTHON_VERSION} \
+ AIRFLOW_IMAGE_FLAVOR=${AIRFLOW_IMAGE_FLAVOR}
ENV RUSTUP_HOME="/usr/local/rustup"
ENV CARGO_HOME="/home/airflow/.cargo"
ENV PATH="${CARGO_HOME}/bin:${PATH}"
COPY --from=scripts install_os_dependencies.sh /scripts/docker/
-COPY scripts/docker/keys/ /scripts/docker/keys/
-RUN PYTHON_LTO=${PYTHON_LTO} bash /scripts/docker/install_os_dependencies.sh dev
+RUN bash /scripts/docker/install_os_dependencies.sh dev
+
+# The final image copies /python-dist over its root. It is empty for the hardened flavor, whose Python
+# comes with the base image, and holds the compiled /usr/python for the legacy flavor - so that COPY
+# needs no condition and adds nothing to the hardened image.
+RUN mkdir -p /python-dist; \
+ if [[ "${AIRFLOW_IMAGE_FLAVOR}" == "legacy" ]]; then \
+ mkdir -p /python-dist/usr && cp -a /usr/python /python-dist/usr/; \
+ fi
# In case system python is installed, setting LD_LIBRARY_PATH prevents any case the system python
# libraries will be accidentally used before the library installed from sources (which is newer and
@@ -1925,6 +2146,7 @@ COPY --from=scripts common.sh /scripts/docker/
# Only copy mysql/mssql installation scripts for now - so that changing the other
# scripts which are needed much later will not invalidate the docker layer here
COPY --from=scripts install_mysql.sh install_mssql.sh install_postgres.sh /scripts/docker/
+COPY scripts/docker/keys/ /scripts/docker/keys/
RUN bash /scripts/docker/install_mysql.sh dev && \
bash /scripts/docker/install_mssql.sh dev && \
@@ -2143,6 +2365,7 @@ ARG INSTALL_MYSQL_CLIENT_TYPE="mariadb"
ARG INSTALL_MSSQL_CLIENT="true"
ARG INSTALL_POSTGRES_CLIENT="true"
ARG AIRFLOW_INSTALLATION_METHOD="apache-airflow"
+ARG AIRFLOW_IMAGE_FLAVOR
ENV RUNTIME_APT_DEPS=${RUNTIME_APT_DEPS} \
ADDITIONAL_RUNTIME_APT_DEPS=${ADDITIONAL_RUNTIME_APT_DEPS} \
@@ -2153,11 +2376,10 @@ ENV RUNTIME_APT_DEPS=${RUNTIME_APT_DEPS} \
INSTALL_MSSQL_CLIENT=${INSTALL_MSSQL_CLIENT} \
INSTALL_POSTGRES_CLIENT=${INSTALL_POSTGRES_CLIENT} \
GUNICORN_CMD_ARGS="--worker-tmp-dir /dev/shm" \
- AIRFLOW_INSTALLATION_METHOD=${AIRFLOW_INSTALLATION_METHOD}
-
-ARG PYTHON_LTO
+ AIRFLOW_INSTALLATION_METHOD=${AIRFLOW_INSTALLATION_METHOD} \
+ AIRFLOW_IMAGE_FLAVOR=${AIRFLOW_IMAGE_FLAVOR}
-COPY --from=airflow-build-image "/usr/python/" "/usr/python/"
+COPY --from=airflow-build-image /python-dist/ /
COPY --from=scripts install_os_dependencies.sh /scripts/docker/
RUN bash /scripts/docker/install_os_dependencies.sh runtime
@@ -2279,6 +2501,7 @@ LABEL org.apache.airflow.distro="debian" \
org.apache.airflow.image="airflow" \
org.apache.airflow.version="${AIRFLOW_VERSION}" \
org.apache.airflow.python.version="${AIRFLOW_PYTHON_VERSION}" \
+ org.apache.airflow.image.flavor="${AIRFLOW_IMAGE_FLAVOR}" \
org.apache.airflow.uid="${AIRFLOW_UID}" \
org.apache.airflow.main-image.build-id="${BUILD_ID}" \
org.apache.airflow.main-image.commit-sha="${COMMIT_SHA}" \
diff --git a/Dockerfile.ci b/Dockerfile.ci
index 754c3650c94..946d7717dcb 100644
--- a/Dockerfile.ci
+++ b/Dockerfile.ci
@@ -18,21 +18,16 @@
#
# WARNING: THIS DOCKERFILE IS NOT INTENDED FOR PRODUCTION USE OR DEPLOYMENT.
#
-# PYTHON_LTO: Controls whether Python is built with Link-Time Optimization (LTO).
+# BASE_IMAGE: the Docker Hardened Image that Python comes from. Both the build stage and the final
+# image are based on it, so Python is never compiled during the build.
#
-# Link-Time Optimization uses MD5 checksums during the compilation process to verify
-# object files and intermediate representations. In FIPS-compliant environments, MD5
-# is blocked as it's not an approved cryptographic algorithm (see FIPS 140-2/140-3).
-# This can cause Python builds with LTO to fail when FIPS mode is enabled.
+# The default is Airflow's public mirror of the upstream "dhi.io/python" image, so building needs no
+# registry credentials. The upstream image itself requires a "docker login dhi.io".
#
-# When building FIPS-compliant images, set this to "false" to disable LTO:
-# docker build --build-arg PYTHON_LTO="false" ...
-#
-# Default: "true" (LTO enabled for better performance)
-#
-# Related: https://github.com/apache/airflow/issues/58337
-ARG PYTHON_LTO="true"
-ARG BASE_IMAGE="debian:bookworm-slim"
+# The FIPS-validated variants require a paid Docker subscription and cannot be mirrored or made the
+# default, but you can build a FIPS-compliant image by pointing the build at one:
+# docker build . --build-arg BASE_IMAGE="dhi.io/python:3.13.16-debian13-fips-dev"
+ARG BASE_IMAGE="ghcr.io/apache/airflow/base/python:3.13.16-debian13-dev"
##############################################################################################
# This is the script image where we keep all inlined bash scripts needed in other segments
@@ -59,15 +54,29 @@ if [[ "$#" != 1 ]]; then
exit 1
fi
-AIRFLOW_PYTHON_VERSION=${AIRFLOW_PYTHON_VERSION:-3.11.16}
-PYTHON_LTO=${PYTHON_LTO:-true}
GOLANG_MAJOR_MINOR_VERSION=${GOLANG_MAJOR_MINOR_VERSION:-1.24.4}
TEMURIN_VERSION=${TEMURIN_VERSION:-11}
NODEJS_VERSION=${NODEJS_VERSION:-22.23.1}
PNPM_VERSION=${PNPM_VERSION:-10.28.1}
RUSTUP_DEFAULT_TOOLCHAIN=${RUSTUP_DEFAULT_TOOLCHAIN:-stable}
RUSTUP_VERSION=${RUSTUP_VERSION:-1.29.0}
+AIRFLOW_IMAGE_FLAVOR=${AIRFLOW_IMAGE_FLAVOR:-hardened}
+AIRFLOW_PYTHON_VERSION=${AIRFLOW_PYTHON_VERSION:-3.13.16}
COSIGN_VERSION=${COSIGN_VERSION:-3.0.5}
+if [[ "${AIRFLOW_IMAGE_FLAVOR}" == "hardened" ]]; then
+ # The Debian 12 hardened images ship Python under /opt/python, the Debian 13 ones install it as
+ # Debian packages under /usr - so ask the base image's Python where it lives.
+ PYTHON_HOME=${PYTHON_HOME:-$(python3 -c 'import sys; print(sys.base_prefix)')}
+ # Read before apt runs: a package depending on python3 can pull another Python version in.
+ BASE_PYTHON_MAJOR_MINOR=$(python3 -c 'import sys; print(f"{sys.version_info[0]}.{sys.version_info[1]}")')
+elif [[ "${AIRFLOW_IMAGE_FLAVOR}" == "legacy" ]]; then
+ PYTHON_HOME=${PYTHON_HOME:-/usr/python}
+else
+ echo
+ echo "ERROR! AIRFLOW_IMAGE_FLAVOR should be 'hardened' or 'legacy', not '${AIRFLOW_IMAGE_FLAVOR}'."
+ echo
+ exit 1
+fi
if [[ "${1}" == "runtime" ]]; then
INSTALLATION_TYPE="RUNTIME"
@@ -94,38 +103,25 @@ freetds-dev \
git \
graphviz \
graphviz-dev \
+gzip \
krb5-user \
-lcov \
ldap-utils \
-libbluetooth-dev \
-libbz2-dev \
libc6-dev \
-libdb-dev \
libev-dev \
libev4 \
libffi-dev \
-libgdbm-compat-dev \
-libgdbm-dev \
libgeos-dev \
libkrb5-dev \
libldap2-dev \
libleveldb-dev \
libleveldb1d \
-liblzma-dev \
-libncurses5-dev \
-libreadline6-dev \
libsasl2-2 \
libsasl2-dev \
libsasl2-modules \
-libsqlite3-dev \
libssl-dev \
libxmlsec1 \
libxmlsec1-dev \
-libzstd-dev \
locales \
-lsb-release \
-lzma \
-lzma-dev \
openssh-client \
openssl \
pkg-config \
@@ -134,14 +130,26 @@ sasl2-bin \
sqlite3 \
sudo \
tdsodbc \
-tk-dev \
unixodbc \
unixodbc-dev \
-uuid-dev \
wget \
xz-utils \
zlib1g-dev \
"
+ if [[ "${AIRFLOW_IMAGE_FLAVOR}" == "legacy" ]]; then
+ # Needed only to compile Python from sources - without them it builds, but silently
+ # lacks modules such as sqlite3, lzma or readline.
+ local debian_version
+ # shellcheck disable=SC1091
+ debian_version=$(. /etc/os-release; printf '%s\n' "$VERSION_CODENAME")
+ if [[ "${debian_version}" == "bookworm" ]]; then
+ DEV_APT_DEPS+="lcov libbluetooth-dev libbz2-dev libdb-dev libgdbm-compat-dev libgdbm-dev \
+liblzma-dev libncurses5-dev libreadline6-dev libsqlite3-dev libzstd-dev lzma lzma-dev tk-dev uuid-dev"
+ else
+ DEV_APT_DEPS+="lcov libbluetooth-dev libbz2-dev libdb-dev libgdbm-compat-dev libgdbm-dev \
+liblzma-dev libncurses-dev libreadline-dev libsqlite3-dev libzstd-dev tk-dev uuid-dev"
+ fi
+ fi
export DEV_APT_DEPS
fi
}
@@ -155,15 +163,29 @@ function get_runtime_apt_deps() {
echo
echo "DEBIAN CODENAME: ${debian_version}"
echo
- debian_version_apt_deps="\
+ if [[ "${debian_version}" == "bookworm" ]]; then
+ debian_version_apt_deps="\
libffi8 \
libldap-2.5-0 \
libssl3 \
netcat-openbsd\
"
+ else
+ # trixie renamed the libraries that moved to a 64-bit time_t (libssl3 -> libssl3t64) and
+ # dropped the soname from the LDAP library package name.
+ debian_version_apt_deps="\
+libffi8 \
+libldap2 \
+libssl3t64 \
+netcat-openbsd\
+"
+ fi
echo
echo "APPLIED INSTALLATION CONFIGURATION FOR DEBIAN VERSION: ${debian_version}"
echo
+ # libxmlsec1-openssl was added because libxmlsec1 ships no crypto engine of its own - the engines
+ # are separate packages - so the "xmlsec" module (pulled in by python3-saml) imported with
+ # "libxmlsec1-openssl.so.1: cannot open shared object file" without it.
if [[ "${RUNTIME_APT_DEPS=}" == "" ]]; then
RUNTIME_APT_DEPS="\
${debian_version_apt_deps} \
@@ -182,8 +204,8 @@ libgeos-dev \
libsasl2-2 \
libsasl2-modules \
libxmlsec1 \
+libxmlsec1-openssl \
locales \
-lsb-release \
openssh-client \
rsync \
sasl2-bin \
@@ -212,10 +234,82 @@ function install_docker_cli() {
apt-get install -y --no-install-recommends docker-ce-cli
}
+function keep_image_conffiles() {
+ # The hardened base images ship a modified /etc/debian_version, so any apt run that upgrades
+ # base-files stops at dpkg's interactive conffile prompt and fails a non-interactive build. Making
+ # dpkg keep the image's copy by default covers every apt run - this build's and those of images
+ # extending it - so no install command has to remember the flags.
+ mkdir -p /etc/dpkg/dpkg.cfg.d
+ printf '%s\n' force-confdef force-confold > /etc/dpkg/dpkg.cfg.d/airflow-keep-conffiles
+}
+
+function refresh_ldconfig_after_dpkg() {
+ # The hardened base images ship libc-bin without its dpkg trigger, so installing a shared library
+ # does not refresh the ldconfig cache and ctypes.util.find_library() - which reads that cache -
+ # cannot find it (for example pyenchant's "The 'enchant' C library was not found"). The hook covers
+ # every apt run - this build's and those of images extending it.
+ mkdir -p /etc/apt/apt.conf.d
+ echo 'DPkg::Post-Invoke { "if [ -x /sbin/ldconfig ]; then /sbin/ldconfig; fi"; };' \
+ > /etc/apt/apt.conf.d/99airflow-ldconfig
+}
+
+function restore_debian_base_files() {
+ # The hardened base images ship a minimal /etc, but Debian maintainer scripts assume the files
+ # a stock Debian has: sasl2-bin chowns its run directory to the "sasl" group from base-passwd,
+ # and tmux registers its shell with add-shell, which reads /etc/shells. The base-passwd package
+ # only ships the reference copies of the account files - update-passwd is what merges them
+ # into /etc.
+ # libpam-runtime generates the /etc/pam.d/common-* files that the PAM configs already in the
+ # image "@include" - without them "adduser --gecos" aborts with a PAM error from chfn.
+ # Debian Policy lets a maintainer script rely on any essential package without declaring it, and
+ # libgcrypt20 takes that up: its postinst runs a helper with a "#!/bin/dash" shebang. The hardened
+ # images ship /bin/sh as a symlink to bash and no dash at all, so configuring the package aborts
+ # with exit 127 and takes the whole apt transaction with it. Installing it first restores the
+ # assumption, and hands /bin/sh back to dash the way a stock Debian has it.
+ apt-get install -y --no-install-recommends dash
+ apt-get install -y --no-install-recommends base-passwd libpam-runtime
+ update-passwd
+ # The Debian 13 hardened images strip /etc/pam.d further: libpam-runtime and passwd are already
+ # installed there, so installing them generates nothing, and passwd, chpasswd and chfn abort with
+ # "pam_start() failed". Reinstalling passwd brings back its deleted PAM configs, and
+ # pam-auth-update - which treats the deleted common-* files as local changes - needs --force.
+ apt-get install -y --no-install-recommends --reinstall -o Dpkg::Options::=--force-confmiss passwd
+ if [[ ! -e /etc/pam.d/common-auth ]]; then
+ pam-auth-update --package --force
+ fi
+ if [[ ! -e /etc/shells ]]; then
+ printf '%s\n' "# /etc/shells: valid login shells" /bin/sh /bin/bash > /etc/shells
+ fi
+ align_perl_base_with_perl
+}
+
+function align_perl_base_with_perl() {
+ # The hardened images ship their own build of perl-base, but the hardened apt repository does not
+ # always publish the matching perl - while it still publishes packages that depend on perl, such
+ # as git. Debian's perl depends on exactly its own perl-base version, so apt refuses to install
+ # git at all. Moving perl-base to the version of the perl apt can install keeps the two in step;
+ # when the repository does publish a matching perl, the versions already agree and this does nothing.
+ local perl_version
+ local perl_base_version
+ perl_version=$(apt-cache policy perl | awk '/Candidate:/ {print $2}')
+ perl_base_version=$(dpkg-query -W -f='${Version}' perl-base 2>/dev/null || true)
+ if [[ -n "${perl_version}" && "${perl_version}" != "(none)" && -n "${perl_base_version}" \
+ && "${perl_base_version}" != "${perl_version}" ]]; then
+ apt-get install -y --no-install-recommends --allow-downgrades "perl-base=${perl_version}"
+ fi
+}
+
function install_debian_dev_dependencies() {
+ if [[ "${AIRFLOW_IMAGE_FLAVOR}" == "hardened" ]]; then
+ keep_image_conffiles
+ refresh_ldconfig_after_dpkg
+ fi
apt-get update
apt-get install -yqq --no-install-recommends apt-utils >/dev/null 2>&1
- apt-get install -y --no-install-recommends wget curl gnupg2 lsb-release ca-certificates
+ if [[ "${AIRFLOW_IMAGE_FLAVOR}" == "hardened" ]]; then
+ restore_debian_base_files
+ fi
+ apt-get install -y --no-install-recommends wget curl gnupg2 ca-certificates
# shellcheck disable=SC2086
export ${ADDITIONAL_DEV_APT_ENV?}
if [[ ${DEV_APT_COMMAND} != "" ]]; then
@@ -245,14 +339,25 @@ function install_additional_dev_dependencies() {
}
function link_python() {
+ # Airflow images have always exposed Python under /usr/python - documentation, volume mounts and
+ # user customizations refer to that path - while the hardened base images ship it in /opt/python
+ # (Debian 12) or /usr (Debian 13), so keep the historical location working as a symlink.
+ if [[ ! -e /usr/python ]]; then
+ ln -sv "${PYTHON_HOME}" /usr/python
+ fi
+ # The hardened base images have no /usr/local tree at all
+ mkdir -p /usr/local/bin /usr/local/lib
# link python binaries to /usr/local/bin and /usr/python/bin with and without 3 suffix
# Links in /usr/local/bin are needed for tools that expect python to be there
# Links in /usr/python/bin are needed for tools that are detecting home of python installation including
# lib/site-packages. The /usr/python/bin should be first in PATH in order to help with the last part.
for dst in pip3 python3 python3-config; do
src="$(echo "${dst}" | tr -d 3)"
+ if [[ ! -e "/usr/python/bin/${dst}" ]]; then
+ continue
+ fi
echo "Linking ${dst} in /usr/local/bin and /usr/python/bin"
- ln -sv "/usr/python/bin/${dst}" "/usr/local/bin/${dst}"
+ ln -sfv "/usr/python/bin/${dst}" "/usr/local/bin/${dst}"
for dir in /usr/local/bin /usr/python/bin; do
if [[ ! -e "${dir}/${src}" ]]; then
echo "Creating ${src} - > ${dst} link in ${dir}"
@@ -260,37 +365,103 @@ function link_python() {
fi
done
done
- for dst in /usr/python/lib/*
- do
- src="/usr/local/lib/$(basename "${dst}")"
- if [[ -e "${src}" ]]; then
- rm -rf "${src}"
- fi
- echo "Linking ${dst} to ${src}"
- ln -sv "${dst}" "${src}"
- done
+ # A Python installed under /usr already has its libraries where the dynamic linker looks, and
+ # linking all of /usr/lib into /usr/local/lib would only shadow the system libraries.
+ if [[ "$(readlink -f "${PYTHON_HOME}")" != "/usr" ]]; then
+ for dst in /usr/python/lib/*
+ do
+ src="/usr/local/lib/$(basename "${dst}")"
+ if [[ -e "${src}" ]]; then
+ rm -rf "${src}"
+ fi
+ echo "Linking ${dst} to ${src}"
+ ln -sv "${dst}" "${src}"
+ done
+ fi
ldconfig
}
-function install_debian_runtime_dependencies() {
- apt-get update
- apt-get install --no-install-recommends -yqq apt-utils >/dev/null 2>&1
- apt-get install -y --no-install-recommends wget curl gnupg2 lsb-release ca-certificates
- # shellcheck disable=SC2086
- export ${ADDITIONAL_RUNTIME_APT_ENV?}
- if [[ "${RUNTIME_APT_COMMAND}" != "" ]]; then
- bash -o pipefail -o errexit -o nounset -o nolog -c "${RUNTIME_APT_COMMAND}"
+function restore_thread_stack_size() {
+ # The hardened base images build Python with -DTHREAD_STACK_SIZE=0x100000, so every thread Python
+ # starts gets a 1 MiB stack instead of glibc's default (the 8 MiB "ulimit -s" the previous images
+ # used). On Python 3.12 and 3.13 the C recursion guard is a fixed depth count sized for the larger
+ # stack, so deeply nested input - e.g. a JSON body parsed in a web server worker thread - overflows
+ # the stack and kills the process instead of raising RecursionError. A .pth file runs at every
+ # interpreter start-up without taking the sitecustomize module name users may already rely on.
+ local site_packages
+ site_packages="$(/usr/python/bin/python -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')"
+ echo "Restoring the 8 MiB default thread stack size in ${site_packages}"
+ echo "import threading; threading.stack_size(8 * 1024 * 1024)" > "${site_packages}/airflow-thread-stack-size.pth"
+}
+
+function compile_python_stdlib() {
+ # The hardened base images ship the standard library with no .pyc files at all. Python then misses
+ # the bytecode cache on every stdlib import, and in a read-only or non-writable directory it cannot
+ # create one - each miss leaves a negative dentry in the kernel, which grows without bound in a
+ # long-running container and can exhaust memory on the host. Compiling the standard library here
+ # restores what the previously compiled-in-image Python shipped.
+ # See https://github.com/apache/airflow/pull/58944 and https://lwn.net/Articles/814535/
+ local stdlib
+ stdlib="$(/usr/python/bin/python -c 'import sysconfig; print(sysconfig.get_paths()["stdlib"])')"
+ echo "Compiling Python standard library in ${stdlib}"
+ # compileall exits non-zero when any file fails to compile, and the standard library ships files
+ # that are meant not to compile (deliberately broken syntax used by the test suite).
+ /usr/python/bin/python -m compileall -q -j "$(nproc)" -o 0 -o 1 -o 2 "${stdlib}" || true
+}
+
+function hold_hardened_python() {
+ # The Debian 13 hardened images install their Python as packages that conflict with Debian's own
+ # python3. A package depending on python3 - for example NodeSource's nodejs - therefore makes apt
+ # remove the image's Python and install Debian's, without asking. Holding the packages turns that
+ # into an apt error instead, in this build and in images extending it.
+ local packages
+ packages=$(dpkg-query -W -f='${db:Status-Abbrev} ${Package}\n' 'python-3.*' 'libpython-3.*' 2>/dev/null \
+ | awk '$1 == "ii" {print $2}' || true)
+ if [[ -n "${packages}" ]]; then
+ # shellcheck disable=SC2086
+ apt-mark hold ${packages}
fi
- if [[ "${ADDITIONAL_RUNTIME_APT_COMMAND}" != "" ]]; then
- bash -o pipefail -o errexit -o nounset -o nolog -c "${ADDITIONAL_RUNTIME_APT_COMMAND}"
+}
+
+function check_no_system_python() {
+ # Python - from the hardened base image or compiled from sources - must stay the only Python in the
+ # image. A system Python pulled in as a dependency of an apt package shares its shared libraries
+ # with ours and leads to errors such as:
+ # /usr/python/lib/python3.11/lib-dynload/_ssl.cpython-311-aarch64-linux-gnu.so: undefined symbol: _PyModule_Add
+ # Debian names its Python libraries "libpython3.13". The trixie hardened images register their own
+ # Python with dpkg as "libpython-3.13", which is the Python we want, so it must not match.
+ # Debian 13 hardened images package their Python as "python-3.13" and serve other versions from
+ # their apt repository, so a package depending on python3 can also pull in a hardened Python of a
+ # different version than the image's own.
+ local other_hardened_python=""
+ if [[ -n "${BASE_PYTHON_MAJOR_MINOR=}" ]]; then
+ other_hardened_python=$(dpkg -l | awk '/^ii (lib)?python-3\.[0-9]+/ {print $2}' \
+ | grep -v -E "python-${BASE_PYTHON_MAJOR_MINOR//./\\.}(-|:|$)" || true)
+ fi
+ if [[ -n "${other_hardened_python}" ]]; then
+ echo
+ echo "ERROR! A Python other than the image's ${BASE_PYTHON_MAJOR_MINOR} was installed: ${other_hardened_python//$'\n'/ }"
+ echo
+ apt-get install -yqq aptitude >/dev/null
+ aptitude why "$(echo "${other_hardened_python}" | head -1 | cut -d: -f1)"
+ echo
+ exit 1
+ fi
+ if dpkg -l | grep -E '^ii libpython3\.[0-9]+' >/dev/null; then
+ echo
+ echo "ERROR! System python is installed by one of the previous steps"
+ echo
+ echo "Please make sure that no python packages are installed by default. Displaying the reason why libpython is installed:"
+ echo
+ apt-get install -yqq aptitude >/dev/null
+ aptitude why "$(dpkg -l | grep -E '^ii libpython3\.[0-9]+' | head -1 | awk '{print $2}')"
+ echo
+ exit 1
+ else
+ echo
+ echo "GOOD! System python is not installed - OK"
+ echo
fi
- apt-get update
- # shellcheck disable=SC2086
- apt-get install -y --no-install-recommends ${RUNTIME_APT_DEPS} ${ADDITIONAL_RUNTIME_APT_DEPS}
- apt-get autoremove -yqq --purge
- apt-get clean
- link_python
- rm -rf /var/lib/apt/lists/* /var/log/*
}
function install_cosign() {
@@ -314,27 +485,7 @@ function install_cosign() {
}
function install_python() {
- # If system python (3.11 in bookworm) is installed (via automatic installation of some dependencies for example), we need
- # to fail and make sure that it is not there, because there can be strange interactions if we install
- # newer version and system libraries are installed, because
- # when you create a virtualenv part of the shared libraries of Python can be taken from the system
- # Installation leading to weird errors when you want to install some modules - for example when you install ssl:
- # /usr/python/lib/python3.11/lib-dynload/_ssl.cpython-311-aarch64-linux-gnu.so: undefined symbol: _PyModule_Add
- if dpkg -l | grep '^ii' | grep '^ii libpython' >/dev/null; then
- echo
- echo "ERROR! System python is installed by one of the previous steps"
- echo
- echo "Please make sure that no python packages are installed by default. Displaying the reason why libpython3.11 is installed:"
- echo
- apt-get install -yqq aptitude >/dev/null
- aptitude why libpython3.11
- echo
- exit 1
- else
- echo
- echo "GOOD! System python is not installed - OK"
- echo
- fi
+ # Only the legacy image flavor compiles Python - the hardened flavor gets it from the base image.
wget --tries=3 --waitretry=5 -O python.tar.xz "https://www.python.org/ftp/python/${AIRFLOW_PYTHON_VERSION%%[a-z]*}/Python-${AIRFLOW_PYTHON_VERSION}.tar.xz"
local major_minor_version
major_minor_version="${AIRFLOW_PYTHON_VERSION%.*}"
@@ -380,21 +531,13 @@ function install_python() {
EXTRA_CFLAGS="${EXTRA_CFLAGS:-} -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer";
LDFLAGS="$(dpkg-buildflags --get LDFLAGS)"
LDFLAGS="${LDFLAGS:--Wl},--strip-all"
- # Link-Time Optimization (LTO) uses MD5 checksums for object file verification during
- # compilation. In FIPS mode, MD5 is blocked as a non-approved algorithm, causing builds
- # to fail. The PYTHON_LTO variable allows disabling LTO for FIPS-compliant builds.
- # See: https://github.com/apache/airflow/issues/58337
- local lto_option=""
- if [[ "${PYTHON_LTO:-true}" == "true" ]]; then
- lto_option="--with-lto"
- fi
local build_log
build_log=$(mktemp)
echo "Building Python ${AIRFLOW_PYTHON_VERSION} from source..."
if ! (
./configure --enable-optimizations --prefix=/usr/python/ --with-ensurepip --build="$gnuArch" \
--enable-loadable-sqlite-extensions --enable-option-checking=fatal \
- --enable-shared ${lto_option} && \
+ --enable-shared --with-lto && \
make -s -j "$(nproc)" "EXTRA_CFLAGS=${EXTRA_CFLAGS:-}" \
"LDFLAGS=${LDFLAGS:--Wl},-rpath='\$\$ORIGIN/../lib'" python && \
make -s -j "$(nproc)" install
@@ -407,6 +550,9 @@ function install_python() {
exit 1
fi
rm -f "${build_log}"
+ # CPython builds without the optional modules whose -dev packages are missing, so check them here
+ # rather than finding out from a broken image.
+ /usr/python/bin/python3 -c "import bz2, ctypes, dbm.gnu, lzma, readline, sqlite3, ssl, uuid, zlib"
cd /
rm -rf /usr/src/python
find /usr/python -depth \
@@ -417,6 +563,40 @@ function install_python() {
link_python
}
+function install_debian_runtime_dependencies() {
+ if [[ "${AIRFLOW_IMAGE_FLAVOR}" == "hardened" ]]; then
+ keep_image_conffiles
+ refresh_ldconfig_after_dpkg
+ fi
+ apt-get update
+ apt-get install --no-install-recommends -yqq apt-utils >/dev/null 2>&1
+ if [[ "${AIRFLOW_IMAGE_FLAVOR}" == "hardened" ]]; then
+ restore_debian_base_files
+ fi
+ apt-get install -y --no-install-recommends wget curl gnupg2 ca-certificates
+ # shellcheck disable=SC2086
+ export ${ADDITIONAL_RUNTIME_APT_ENV?}
+ if [[ "${RUNTIME_APT_COMMAND}" != "" ]]; then
+ bash -o pipefail -o errexit -o nounset -o nolog -c "${RUNTIME_APT_COMMAND}"
+ fi
+ if [[ "${ADDITIONAL_RUNTIME_APT_COMMAND}" != "" ]]; then
+ bash -o pipefail -o errexit -o nounset -o nolog -c "${ADDITIONAL_RUNTIME_APT_COMMAND}"
+ fi
+ apt-get update
+ # shellcheck disable=SC2086
+ apt-get install -y --no-install-recommends ${RUNTIME_APT_DEPS} ${ADDITIONAL_RUNTIME_APT_DEPS}
+ apt-get autoremove -yqq --purge
+ apt-get clean
+ check_no_system_python
+ link_python
+ if [[ "${AIRFLOW_IMAGE_FLAVOR}" == "hardened" ]]; then
+ hold_hardened_python
+ restore_thread_stack_size
+ compile_python_stdlib
+ fi
+ rm -rf /var/lib/apt/lists/* /var/log/*
+}
+
function install_golang() {
curl --retry 3 --retry-delay 5 "https://dl.google.com/go/go${GOLANG_MAJOR_MINOR_VERSION}.linux-$(dpkg --print-architecture).tar.gz" -o "go${GOLANG_MAJOR_MINOR_VERSION}.linux.tar.gz"
rm -rf /usr/local/go && tar -C /usr/local -xzf go"${GOLANG_MAJOR_MINOR_VERSION}".linux.tar.gz
@@ -513,7 +693,15 @@ if [[ "${INSTALLATION_TYPE}" == "RUNTIME" ]]; then
else
get_dev_apt_deps
install_debian_dev_dependencies
- install_python
+ check_no_system_python
+ if [[ "${AIRFLOW_IMAGE_FLAVOR}" == "hardened" ]]; then
+ link_python
+ hold_hardened_python
+ restore_thread_stack_size
+ compile_python_stdlib
+ else
+ install_python
+ fi
install_additional_dev_dependencies
install_rustup
if [[ "${INSTALLATION_TYPE}" == "CI" ]]; then
@@ -536,7 +724,6 @@ set -euo pipefail
common::get_colors
declare -a packages
-readonly MARIADB_LTS_VERSION="10.11"
: "${INSTALL_MYSQL_CLIENT:?Should be true or false}"
: "${INSTALL_MYSQL_CLIENT_TYPE:-mariadb}"
@@ -590,6 +777,13 @@ retry() {
}
install_mariadb_client() {
+ # https://mariadb.org/about/#maintenance-policy
+ # MariaDB publishes each LTS series only for the Debian releases current when it came out - 10.11
+ # has no trixie repository and 11.8 is the first LTS that does.
+ local mariadb_lts_version="10.11"
+ if [[ "$(common::debian_codename)" != "bookworm" ]]; then
+ mariadb_lts_version="11.8"
+ fi
# List of compatible package Oracle MySQL -> MariaDB:
# `mysql-client` -> `mariadb-client` or `mariadb-client-compat` (11+)
# `libmysqlclientXX` (where XX is a number) -> `libmariadb3-compat`
@@ -609,15 +803,19 @@ install_mariadb_client() {
echo
exit 1
fi
+ if [[ "${mariadb_lts_version%%.*}" -ge 11 ]]; then
+ # From 11.0 mariadb-client no longer ships the "mysql" named commands
+ packages+=("mariadb-client-compat")
+ fi
common::import_trusted_gpg "0xF1656F24C74CD1D8" "mariadb"
echo
- echo "${COLOR_BLUE}Installing MariaDB client version ${MARIADB_LTS_VERSION}: ${1}${COLOR_RESET}"
+ echo "${COLOR_BLUE}Installing MariaDB client version ${mariadb_lts_version}: ${1}${COLOR_RESET}"
echo "${COLOR_YELLOW}MariaDB client protocol-compatible with MySQL client.${COLOR_RESET}"
echo
- echo "deb [arch=amd64,arm64] https://archive.mariadb.org/mariadb-${MARIADB_LTS_VERSION}/repo/debian/ $(lsb_release -cs) main" > \
+ echo "deb [arch=amd64,arm64] https://archive.mariadb.org/mariadb-${mariadb_lts_version}/repo/debian/ $(common::debian_codename) main" > \
/etc/apt/sources.list.d/mariadb.list
# Make sure that dependencies from MariaDB repo are preferred over Debian dependencies
printf "Package: *\nPin: release o=MariaDB\nPin-Priority: 999\n" > /etc/apt/preferences.d/mariadb
@@ -656,12 +854,14 @@ function install_mssql_client() {
packages=("msodbcsql18")
common::import_trusted_gpg "EB3E94ADBE1229CF" "microsoft"
+ # The Debian 13 repository is signed with Microsoft's newer key, the older releases with the one above.
+ common::import_trusted_gpg "EE4D7792F748182B" "microsoft-2025"
echo
echo "${COLOR_BLUE}Installing mssql client${COLOR_RESET}"
echo
- echo "deb [arch=amd64,arm64] https://packages.microsoft.com/debian/$(lsb_release -rs)/prod $(lsb_release -cs) main" > \
+ echo "deb [arch=amd64,arm64] https://packages.microsoft.com/debian/$(common::debian_release)/prod $(common::debian_codename) main" > \
/etc/apt/sources.list.d/mssql-release.list &&
mkdir -p /opt/microsoft/msodbcsql18 &&
touch /opt/microsoft/msodbcsql18/ACCEPT_EULA &&
@@ -705,7 +905,7 @@ install_postgres_client() {
common::import_trusted_gpg "7FCC7D46ACCC4CF8" "postgres"
- echo "deb [arch=amd64,arm64] https://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > \
+ echo "deb [arch=amd64,arm64] https://apt.postgresql.org/pub/repos/apt/ $(common::debian_codename)-pgdg main" > \
/etc/apt/sources.list.d/pgdg.list
apt-get update
apt-get install --no-install-recommends -y "${packages[@]}"
@@ -852,12 +1052,12 @@ function common::get_constraints_location() {
function common::show_packaging_tool_version_and_location() {
echo "PATH=${PATH}"
- echo "Installed pip: $(pip --version): $(which pip)"
+ echo "Installed pip: $(pip --version): $(command -v pip)"
if [[ ${PACKAGING_TOOL} == "pip" ]]; then
echo "${COLOR_BLUE}Using 'pip' to install Airflow${COLOR_RESET}"
else
echo "${COLOR_BLUE}Using 'uv' to install Airflow${COLOR_RESET}"
- echo "Installed uv: $(uv --version 2>/dev/null || echo "Not installed yet"): $(which uv 2>/dev/null)"
+ echo "Installed uv: $(uv --version 2>/dev/null || echo "Not installed yet"): $(command -v uv 2>/dev/null)"
fi
}
@@ -869,7 +1069,7 @@ function common::install_packaging_tools() {
echo
else
echo
- echo "${COLOR_BLUE}Checking packaging tools for system Python installation: $(which python)${COLOR_RESET}"
+ echo "${COLOR_BLUE}Checking packaging tools for system Python installation: $(command -v python)${COLOR_RESET}"
echo
fi
if [[ ${AIRFLOW_PIP_VERSION=} == "" ]]; then
@@ -933,6 +1133,16 @@ function common::install_packaging_tools() {
fi
}
+function common::debian_codename() {
+ # shellcheck disable=SC1091
+ (. /etc/os-release; printf '%s\n' "${VERSION_CODENAME}")
+}
+
+function common::debian_release() {
+ # shellcheck disable=SC1091
+ (. /etc/os-release; printf '%s\n' "${VERSION_ID}")
+}
+
function common::import_trusted_gpg() {
common::get_colors
@@ -1319,8 +1529,9 @@ SSH_CONFIG
sudo service ssh restart >/dev/null 2>&1
- # Sometimes the server is not quick enough to load the keys!
- while [[ $(ssh-keyscan -H localhost 2>/dev/null | wc -l) != "3" ]] ; do
+ # Sometimes the server is not quick enough to load the keys! OpenSSH 10 also prints a
+ # "# localhost:22 SSH-2.0-..." comment line per connection to stdout, so count only the keys.
+ while [[ $(ssh-keyscan -H localhost 2>/dev/null | grep -cv '^#') != "3" ]] ; do
echo "Not all keys yet loaded by the server"
sleep 0.05
done
@@ -1739,15 +1950,25 @@ ENV BASE_IMAGE=${BASE_IMAGE} \
PIP_CACHE_DIR=/root/.cache/pip \
UV_CACHE_DIR=/root/.cache/uv
+# GCC 14 in trixie turns incompatible-pointer-types, int-conversion and implicit function declarations
+# into errors, and the hardened Python passes -Werror=implicit-function-declaration on to C extensions.
+# The lowest-dependency tests build old sdists (for example xmlsec 1.3.14 or ibm-db 3.2.0) that only
+# compiled because those were warnings - extensions append CFLAGS after Python's own flags, so this
+# turns them back into warnings for every build in the CI image.
+ENV CFLAGS="-Wno-error=incompatible-pointer-types -Wno-error=int-conversion -Wno-error=implicit-function-declaration"
+
RUN echo "Base image version: ${BASE_IMAGE}"
ARG DEV_APT_COMMAND=""
ARG ADDITIONAL_DEV_APT_COMMAND=""
ARG ADDITIONAL_DEV_ENV_VARS=""
+# No libenchant-2-dev: pyenchant finds libenchant-2-2 through the ldconfig cache, and on trixie the
+# -dev package's build dependencies pull in a second Python from the hardened images' repository.
ARG ADDITIONAL_DEV_APT_DEPS="bash-completion dumb-init git gdb graphviz krb5-user \
- less libenchant-2-2 libgcc-11-dev libgeos-dev libpq-dev net-tools netcat-openbsd \
- openssh-server postgresql-client software-properties-common rsync tmux unzip vim xxd"
+ less libenchant-2-2 libgeos-dev libpq-dev net-tools \
+ netcat-openbsd openssh-server postgresql-client rsync tmux \
+ unzip vim xxd"
ARG ADDITIONAL_DEV_APT_ENV=""
@@ -1767,12 +1988,9 @@ ENV RUSTUP_HOME="/usr/local/rustup"
ENV CARGO_HOME="/usr/local/cargo"
ENV PATH="${CARGO_HOME}/bin:${PATH}"
-ARG PYTHON_LTO
-
COPY --from=scripts install_os_dependencies.sh /scripts/docker/
-COPY scripts/docker/keys/ /scripts/docker/keys/
-RUN PYTHON_LTO=${PYTHON_LTO} bash /scripts/docker/install_os_dependencies.sh ci
+RUN bash /scripts/docker/install_os_dependencies.sh ci
# In case system python is installed, setting LD_LIBRARY_PATH prevents any case the system python
# libraries will be accidentally used before the library installed from sources (which is newer and
@@ -1784,6 +2002,7 @@ COPY --from=scripts common.sh /scripts/docker/
# Only copy mysql/mssql installation scripts for now - so that changing the other
# scripts which are needed much later will not invalidate the docker layer here.
COPY --from=scripts install_mysql.sh install_mssql.sh install_postgres.sh /scripts/docker/
+COPY scripts/docker/keys/ /scripts/docker/keys/
ARG HOME=/root
ARG AIRFLOW_IMAGE_TYPE="ci"
diff --git a/airflow-core/newsfragments/73038.significant.rst b/airflow-core/newsfragments/73038.significant.rst
new file mode 100644
index 00000000000..e10012400d0
--- /dev/null
+++ b/airflow-core/newsfragments/73038.significant.rst
@@ -0,0 +1,31 @@
+Airflow's CI and production images are now based on `Docker Hardened Images <https://dhi.io>`_ for Python and Debian 13 "trixie"
+
+Python is no longer compiled from source during the image build - it comes from the hardened base image
+instead, which removes the source download, its signature verification and the compilation itself from the
+build (the OS dependency layer of the production image went from roughly 210s to 105s in a local
+cache-disabled build). Python still lives in ``/usr/python``, which is now a symlink to the ``/opt/python``
+location the base image uses, so paths inside the image are unchanged.
+
+The hardened images ship the standard library without any ``.pyc`` files, so the image build compiles it.
+Without that, every standard library import would miss the bytecode cache and be unable to populate it -
+the standard library is owned by root and the image runs as ``airflow`` - and each miss leaks a negative
+dentry, which is the memory growth that was fixed for the previous images in #58944.
+
+The ``BASE_IMAGE`` build argument now defaults to ``ghcr.io/apache/airflow/base/python:<version>-debian13-dev``,
+Airflow's public mirror of the upstream ``dhi.io/python`` image - building an image needs no registry
+credentials. Anyone overriding ``BASE_IMAGE`` with a plain ``debian:bookworm-slim`` image has to switch to
+an image that already provides Python, or build the ``legacy`` flavor.
+
+The base OS moved from Debian 12 "bookworm" to Debian 13 "trixie". Library packages renamed in trixie's
+64-bit ``time_t`` transition (for example ``libssl3`` is now ``libssl3t64``) have to be updated in custom
+``RUNTIME_APT_DEPS`` / ``ADDITIONAL_RUNTIME_APT_DEPS``, and the MySQL-compatible client now comes from
+MariaDB 11.8.
+
+For the 3.4.x line, production images built the previous way - Python compiled from sources on
+``debian:bookworm-slim`` - are still published as the deprecated ``legacy`` flavor, selected with the new
+``AIRFLOW_IMAGE_FLAVOR="legacy"`` build argument. They will be removed in Airflow 3.5.0.
+
+The ``PYTHON_LTO`` build argument is gone. It existed to disable Link-Time Optimization when compiling
+Python in FIPS mode; the legacy flavor always compiles with LTO. To build a FIPS-compliant image,
+point ``BASE_IMAGE`` at a FIPS variant of the hardened image instead, for example
+``dhi.io/python:3.13.16-debian13-fips-dev``.
diff --git a/airflow-e2e-tests/docker/Dockerfile.java b/airflow-e2e-tests/docker/Dockerfile.java
index 68ccff525a3..1c166cee225 100644
--- a/airflow-e2e-tests/docker/Dockerfile.java
+++ b/airflow-e2e-tests/docker/Dockerfile.java
@@ -19,12 +19,31 @@
# can spawn JVM subprocesses for @task.stub tasks.
#
# Pin Java 17 (rather than default-jre-headless): the Scala Spark example runs
-# Apache Spark 3.5.x, which supports Java 8/11/17 but not Java 21.
+# Apache Spark 3.5.x, which supports Java 8/11/17 but not Java 21. Debian trixie ships
+# no Java 17, so it comes from the Eclipse Temurin (Adoptium) repository.
ARG DOCKER_IMAGE
FROM ${DOCKER_IMAGE}
+# "Adoptium GPG Key (DEB/RPM Signing Key)", published at
+# https://adoptium.net/installation/gpg-verification/
+ARG ADOPTIUM_KEY_FINGERPRINT="3B04D753C9050D9A5D343F39843C48A565F8F04B"
+
+SHELL ["/bin/bash", "-o", "pipefail", "-o", "errexit", "-o", "nounset", "-c"]
USER root
RUN apt-get update \
- && apt-get install -y --no-install-recommends openjdk-17-jre-headless \
+ && apt-get install -y --no-install-recommends ca-certificates curl gnupg \
+ && mkdir -p /etc/apt/keyrings \
+ && curl -fsSL --retry 3 https://packages.adoptium.net/artifactory/api/gpg/key/public \
+ -o /etc/apt/keyrings/adoptium.asc \
+ && fingerprint="$(gpg --show-keys --with-colons /etc/apt/keyrings/adoptium.asc \
+ | awk -F: '/^fpr:/ {print $10; exit}')" \
+ && if [ "${fingerprint}" != "${ADOPTIUM_KEY_FINGERPRINT}" ]; then \
+ echo "Adoptium key fingerprint '${fingerprint}' != expected '${ADOPTIUM_KEY_FINGERPRINT}'" >&2; \
+ exit 1; \
+ fi \
+ && echo "deb [signed-by=/etc/apt/keyrings/adoptium.asc] https://packages.adoptium.net/artifactory/deb \
+$(. /etc/os-release && echo "${VERSION_CODENAME}") main" > /etc/apt/sources.list.d/adoptium.list \
+ && apt-get update \
+ && apt-get install -y --no-install-recommends temurin-17-jre \
&& rm -rf /var/lib/apt/lists/*
USER airflow
diff --git a/dev/breeze/doc/09_release_management_tasks.rst b/dev/breeze/doc/09_release_management_tasks.rst
index 25caf147ac8..4d22d9a61ca 100644
--- a/dev/breeze/doc/09_release_management_tasks.rst
+++ b/dev/breeze/doc/09_release_management_tasks.rst
@@ -1255,6 +1255,34 @@ Example usage:
breeze release-management constraints-version-check --python 3.11 --airflow-constraints-mode constraints-source-providers --explain-why
+Mirror the hardened Python base images
+""""""""""""""""""""""""""""""""""""""
+
+The CI and PROD images are built on a Docker Hardened Image for Python. Pulling that image from
+its origin requires a Docker Hub login, so the tags Airflow builds on are mirrored to Airflow's
+own public registry and ``BASE_IMAGE`` points there - building the images then needs no registry
+credentials at all. A scheduled workflow keeps the mirror current; this command is what it runs,
+and what you run by hand when a new Python patch release needs mirroring straight away.
+
+Both the pinned tag and the floating major/minor tag are mirrored, so documentation and ad-hoc
+builds can keep referring to the floating one.
+
+These are all available flags of ``release-management mirror-base-images`` command:
+
+.. image:: ./images/output_release-management_mirror-base-images.svg
+ :target: https://raw.githubusercontent.com/apache/airflow/main/dev/breeze/doc/images/output_release-management_mirror-base-images.svg
+ :width: 100%
+ :alt: Breeze mirror base images
+
+Example usage:
+
+.. code-block:: bash
+
+ breeze release-management mirror-base-images --python 3.10
+
+Without ``--python`` every currently supported Python version is mirrored.
+
+
-----
Next step: Follow the `UI Tasks <10_ui_tasks.rst>`_ to learn more about UI tasks.
diff --git a/dev/breeze/doc/ci/02_images.md b/dev/breeze/doc/ci/02_images.md
index 9fc62bd5e02..341acf6ea59 100644
--- a/dev/breeze/doc/ci/02_images.md
+++ b/dev/breeze/doc/ci/02_images.md
@@ -379,7 +379,7 @@ plugin installed.
``` bash
DOCKER_BUILDKIT=1 docker build . -f Dockerfile.ci \
--pull \
- --build-arg PYTHON_BASE_IMAGE="python:3.11-slim-bookworm" \
+ --build-arg BASE_IMAGE="ghcr.io/apache/airflow/base/python:3.11-debian13-dev" \
--build-arg ADDITIONAL_AIRFLOW_EXTRAS="jdbc" \
--build-arg ADDITIONAL_PYTHON_DEPS="pandas" \
--build-arg ADDITIONAL_DEV_APT_DEPS="gcc g++" \
@@ -404,7 +404,7 @@ comment](https://github.com/apache/airflow/issues/8605#issuecomment-690065621):
``` bash
DOCKER_BUILDKIT=1 docker build . -f Dockerfile.ci \
--pull \
- --build-arg PYTHON_BASE_IMAGE="python:3.11-slim-bookworm" \
+ --build-arg BASE_IMAGE="ghcr.io/apache/airflow/base/python:3.11-debian13-dev" \
--build-arg AIRFLOW_INSTALLATION_METHOD="apache-airflow" \
--build-arg ADDITIONAL_AIRFLOW_EXTRAS="slack" \
--build-arg ADDITIONAL_PYTHON_DEPS="apache-airflow-providers-odbc \
@@ -429,7 +429,7 @@ can be used for CI images:
| Build argument | Default value | Description |
|-----------------------------------|-----------------------------|-------------------------------------------------------------------------------------------------------------------|
-| `PYTHON_BASE_IMAGE` | `python:3.11-slim-bookworm` | Base Python image |
+| `BASE_IMAGE` | `ghcr.io/apache/airflow/base/python:<AIRFLOW_PYTHON_VERSION>-debian13-dev` | Mirror of the hardened base image Python comes from |
| `PYTHON_MAJOR_MINOR_VERSION` | `3.11` | major/minor version of Python (should match base image) |
| `DEPENDENCIES_EPOCH_NUMBER` | `2` | increasing this number will reinstall all apt dependencies |
| `ADDITIONAL_PIP_INSTALL_FLAGS` | | additional `pip` flags passed to the installation commands (except when reinstalling `pip` itself) |
@@ -465,7 +465,7 @@ This builds the CI image in version 3.11 with default extras ("all").
``` bash
DOCKER_BUILDKIT=1 docker build . -f Dockerfile.ci \
--pull \
- --build-arg PYTHON_BASE_IMAGE="python:3.11-slim-bookworm" --tag my-image:0.0.1
+ --build-arg BASE_IMAGE="ghcr.io/apache/airflow/base/python:3.11-debian13-dev" --tag my-image:0.0.1
```
This builds the CI image in version 3.11 with "gcp" extra only.
@@ -473,7 +473,7 @@ This builds the CI image in version 3.11 with "gcp" extra only.
``` bash
DOCKER_BUILDKIT=1 docker build . -f Dockerfile.ci \
--pull \
- --build-arg PYTHON_BASE_IMAGE="python:3.11-slim-bookworm" \
+ --build-arg BASE_IMAGE="ghcr.io/apache/airflow/base/python:3.11-debian13-dev" \
--build-arg AIRFLOW_EXTRAS=gcp --tag my-image:0.0.1
```
@@ -482,7 +482,7 @@ This builds the CI image in version 3.11 with "apache-beam" extra added.
``` bash
DOCKER_BUILDKIT=1 docker build . -f Dockerfile.ci \
--pull \
- --build-arg PYTHON_BASE_IMAGE="python:3.11-slim-bookworm" \
+ --build-arg BASE_IMAGE="ghcr.io/apache/airflow/base/python:3.11-debian13-dev" \
--build-arg ADDITIONAL_AIRFLOW_EXTRAS="apache-beam" --tag my-image:0.0.1
```
@@ -492,7 +492,7 @@ added.
``` bash
DOCKER_BUILDKIT=1 docker build . -f Dockerfile.ci \
--pull \
- --build-arg PYTHON_BASE_IMAGE="python:3.11-slim-bookworm" \
+ --build-arg BASE_IMAGE="ghcr.io/apache/airflow/base/python:3.11-debian13-dev" \
--build-arg ADDITIONAL_PYTHON_DEPS="mssql" --tag my-image:0.0.1
```
@@ -502,7 +502,7 @@ apt dev dependencies added.
```
DOCKER_BUILDKIT=1 docker build . -f Dockerfile.ci \
--pull
- --build-arg PYTHON_BASE_IMAGE="python:3.11-slim-bookworm" \
+ --build-arg BASE_IMAGE="ghcr.io/apache/airflow/base/python:3.11-debian13-dev" \
--build-arg ADDITIONAL_DEV_APT_DEPS="gcc g++" --tag my-image:0.0.1
```
@@ -512,7 +512,7 @@ This builds the CI image in version 3.11 with "jdbc" extra and
```
DOCKER_BUILDKIT=1 docker build . -f Dockerfile.ci \
--pull \
- --build-arg PYTHON_BASE_IMAGE="python:3.11-slim-bookworm" \
+ --build-arg BASE_IMAGE="ghcr.io/apache/airflow/base/python:3.11-debian13-dev" \
--build-arg AIRFLOW_EXTRAS=jdbc \
--tag my-image:0.0.1
```
@@ -569,7 +569,7 @@ percent-encoded when you access them via UI (/ = %2F)
| Image | Name | Description |
|--------------------------|----------------------------------------|---------------------------------------------------------------|
-| Python image (DockerHub) | python:\<X.Y\>-slim-bookworm | Base Python image used by both production and CI image. |
+| Hardened Python base | base/python:\<X.Y.Z\>-debian13-dev | Mirror of the Docker Hardened Python image that the CI image and the default PROD image are built on. |
| CI image | airflow/\<BRANCH\>/ci/python\<X.Y\> | CI image - this is the image used for most of the tests. |
| PROD image | airflow/\<BRANCH\>/prod/python\<X.Y\> | faster to build or pull. Production image optimized for size. |
diff --git a/dev/breeze/doc/images/output_ci-image_build.svg b/dev/breeze/doc/images/output_ci-image_build.svg
index 0e16e89fe18..11a4b79563e 100644
--- a/dev/breeze/doc/images/output_ci-image_build.svg
+++ b/dev/breeze/doc/images/output_ci-image_build.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 1482 2026.3999999999999" xmlns="http://www.w3.org/2000/svg">
+<svg class="rich-terminal" viewBox="0 0 1482 1977.6" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@@ -43,7 +43,7 @@
<defs>
<clipPath id="breeze-ci-image-build-clip-terminal">
- <rect x="0" y="0" width="1463.0" height="1975.3999999999999" />
+ <rect x="0" y="0" width="1463.0" height="1926.6" />
</clipPath>
<clipPath id="breeze-ci-image-build-line-0">
<rect x="0" y="1.5" width="1464" height="24.65"/>
@@ -279,15 +279,9 @@
<clipPath id="breeze-ci-image-build-line-77">
<rect x="0" y="1880.3" width="1464" height="24.65"/>
</clipPath>
-<clipPath id="breeze-ci-image-build-line-78">
- <rect x="0" y="1904.7" width="1464" height="24.65"/>
- </clipPath>
-<clipPath id="breeze-ci-image-build-line-79">
- <rect x="0" y="1929.1" width="1464" height="24.65"/>
- </clipPath>
</defs>
- <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1480" height="2024.4" rx="8"/><text class="breeze-ci-image-build-title" fill="#c5c8c6" text-anchor="middle" x="740" y="27">Command: ci-image build</text>
+ <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1480" height="1975.6" rx="8"/><text class="breeze-ci-image-build-title" fill="#c5c8c6" text-anchor="middle" x="740" y="27">Command: ci-image build</text>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
@@ -336,48 +330,46 @@
</text><text class="breeze-ci-image-build-r5" x="0" y="898.4" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-36)">│</text><text class="breeze-ci-image-build-r1" x="414.8" y="898.4" textLength="1024.8" clip-path="url(#breeze-ci-image-build-line-36)">registry, an image only present in the local docker engine contributes no cache.    </text><text class="breeze-ci-image-build-r5" x="1451.8" y="898.4 [...]
</text><text class="breeze-ci-image-build-r5" x="0" y="922.8" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-37)">│</text><text class="breeze-ci-image-build-r7" x="414.8" y="922.8" textLength="73.2" clip-path="url(#breeze-ci-image-build-line-37)">(TEXT)</text><text class="breeze-ci-image-build-r5" x="1451.8" y="922.8" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-37)">│</text><text class="breeze-ci-image-build-r1" x="1464" y="922.8" textLength="12.2" clip-pat [...]
</text><text class="breeze-ci-image-build-r5" x="0" y="947.2" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-38)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="947.2" textLength="366" clip-path="url(#breeze-ci-image-build-line-38)">--commit-sha                  </text><text class="breeze-ci-image-build-r1" x="414.8" y="947.2" textLength="549" clip-path="url(#breeze-ci-imag [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="971.6" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-39)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="971.6" textLength="366" clip-path="url(#breeze-ci-image-build-line-39)">--debian-version              </text><text class="breeze-ci-image-build-r1" x="414.8" y="971.6" textLength="805.2" clip-path="url(#breeze-ci-image-build-line-39)"> [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="996" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-40)">│</text><text class="breeze-ci-image-build-r5" x="414.8" y="996" textLength="109.8" clip-path="url(#breeze-ci-image-build-line-40)">bookworm]</text><text class="breeze-ci-image-build-r7" x="536.8" y="996" textLength="122" clip-path="url(#breeze-ci-image-build-line-40)">(bookworm)</text><text class="breeze-ci-image-build-r5" x="1451.8" y="996" textLength="12.2" cli [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="1020.4" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-41)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1020.4" textLength="366" clip-path="url(#breeze-ci-image-build-line-41)">--disable-airflow-repo-cache  </text><text class="breeze-ci-image-build-r1" x="414.8" y="1020.4" textLength="658.8" clip-path="url(#breeze-ci-image-build-line-41)">Disable cache from Airflow repository [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="1044.8" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-42)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1044.8" textLength="366" clip-path="url(#breeze-ci-image-build-line-42)">--install-mysql-client-type   </text><text class="breeze-ci-image-build-r1" x="414.8" y="1044.8" textLength="488" clip-path="url(#breeze-ci-image-build-line-42)">Which client to choose when i [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="1069.2" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-43)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1069.2" textLength="366" clip-path="url(#breeze-ci-image-build-line-43)">--python-image                </text><text class="breeze-ci-image-build-r1" x="414.8" y="1069.2" textLength="1024.8" clip-path="url(#breeze-ci-image-bu [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="1093.6" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-44)">│</text><text class="breeze-ci-image-build-r1" x="414.8" y="1093.6" textLength="561.2" clip-path="url(#breeze-ci-image-build-line-44)">something like: python:VERSION-slim-bookworm. </text><text class="breeze-ci-image-build-r7" x="976" y="1093.6" textLength="73.2" clip-path="url(#breeze-ci-image-build-line-44)">(TEXT)</text><text class="breeze-ci- [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="1118" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-45)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1118" textLength="97.6" clip-path="url(#breeze-ci-image-build-line-45)">--use-uv</text><text class="breeze-ci-image-build-r1" x="122" y="1118" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-45)">/</text><text class="breeze-ci-image-build-r4" x="134.2" y="1118" textLength="134.2" clip-path="u [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="1142.4" textLength="1464" clip-path="url(#breeze-ci-image-build-line-46)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-ci-image-build-r1" x="1464" y="1142.4" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-46)">
-</text><text class="breeze-ci-image-build-r5" x="0" y="1166.8" textLength="24.4" clip-path="url(#breeze-ci-image-build-line-47)">â•─</text><text class="breeze-ci-image-build-r5" x="24.4" y="1166.8" textLength="597.8" clip-path="url(#breeze-ci-image-build-line-47)"> Selecting constraint location (for power users) </text><text class="breeze-ci-image-build-r5" x="622.2" y="1166.8" textLength="817.4" clip-path="url(#breeze-ci-image-build-line-47)">────────── [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="1191.2" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-48)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1191.2" textLength="378.2" clip-path="url(#breeze-ci-image-build-line-48)">--airflow-constraints-location </text><text class="breeze-ci-image-build-r1" x="427" y="1191.2" textLength="915" clip-path="url(#breeze-ci-image-build-line-48)">Location of airflow constraints to use [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="1215.6" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-49)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1215.6" textLength="378.2" clip-path="url(#breeze-ci-image-build-line-49)">--airflow-constraints-mode     </text><text class="breeze-ci-image-build-r1" x="427" y="1215.6" textLength="671" clip-path="url(#breeze-ci-image-build-line-49)">Mode of constraints for  [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="1240" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-50)">│</text><text class="breeze-ci-image-build-r5" x="427" y="1240" textLength="353.8" clip-path="url(#breeze-ci-image-build-line-50)">constraints-source-providers]</text><text class="breeze-ci-image-build-r7" x="793" y="1240" textLength="561.2" clip-path="url(#breeze-ci-image-build-line-50)">(constraints-source-providers | constraints | </text><t [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="1264.4" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-51)">│</text><text class="breeze-ci-image-build-r7" x="427" y="1264.4" textLength="305" clip-path="url(#breeze-ci-image-build-line-51)">constraints-no-providers)</text><text class="breeze-ci-image-build-r5" x="1451.8" y="1264.4" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-51)">│</text><text class="breeze-ci-image-build-r1" x="1464" y="1264.4" textLe [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="1288.8" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-52)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1288.8" textLength="378.2" clip-path="url(#breeze-ci-image-build-line-52)">--airflow-constraints-reference</text><text class="breeze-ci-image-build-r1" x="427" y="1288.8" textLength="646.6" clip-path="url(#breeze-ci-image-build-line-52)">Constraint reference to use when building [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="1313.2" textLength="1464" clip-path="url(#breeze-ci-image-build-line-53)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-ci-image-build-r1" x="1464" y="1313.2" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-53)">
-</text><text class="breeze-ci-image-build-r5" x="0" y="1337.6" textLength="24.4" clip-path="url(#breeze-ci-image-build-line-54)">â•─</text><text class="breeze-ci-image-build-r5" x="24.4" y="1337.6" textLength="634.4" clip-path="url(#breeze-ci-image-build-line-54)"> Choosing dependencies and extras (for power users) </text><text class="breeze-ci-image-build-r5" x="658.8" y="1337.6" textLength="780.8" clip-path="url(#breeze-ci-image-build-line-54)">── [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="1362" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-55)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1362" textLength="341.6" clip-path="url(#breeze-ci-image-build-line-55)">--additional-airflow-extras </text><text class="breeze-ci-image-build-r1" x="390.4" y="1362" textLength="780.8" clip-path="url(#breeze-ci-image-build-line-55)">Additional extra package while installing A [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="1386.4" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-56)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1386.4" textLength="341.6" clip-path="url(#breeze-ci-image-build-line-56)">--additional-python-deps    </text><text class="breeze-ci-image-build-r1" x="390.4" y="1386.4" textLength="780.8" clip-path="url(#breeze-ci-image-build-line-56)">Additional python dependencies t [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="1410.8" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-57)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1410.8" textLength="341.6" clip-path="url(#breeze-ci-image-build-line-57)">--dev-apt-deps              </text><text class="breeze-ci-image-build-r1" x="390.4" y="1410.8" textLength="658.8" clip-path="url(#breeze-ci-image-build-line-57 [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="1435.2" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-58)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1435.2" textLength="341.6" clip-path="url(#breeze-ci-image-build-line-58)">--additional-dev-apt-deps   </text><text class="breeze-ci-image-build-r1" x="390.4" y="1435.2" textLength="793" clip-path="url(#breeze-ci-image-build-line-58)">Additional apt dev dependencies to [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="1459.6" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-59)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1459.6" textLength="341.6" clip-path="url(#breeze-ci-image-build-line-59)">--dev-apt-command           </text><text class="breeze-ci-image-build-r1" x="390.4" y="1459.6" textLength="634.4" clip-path="url(#breeze-ci-image-build-line-59)">Command  [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="1484" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-60)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1484" textLength="341.6" clip-path="url(#breeze-ci-image-build-line-60)">--additional-dev-apt-command</text><text class="breeze-ci-image-build-r1" x="390.4" y="1484" textLength="768.6" clip-path="url(#breeze-ci-image-build-line-60)">Additional command executed before dev apt  [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="1508.4" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-61)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1508.4" textLength="341.6" clip-path="url(#breeze-ci-image-build-line-61)">--additional-dev-apt-env    </text><text class="breeze-ci-image-build-r1" x="390.4" y="1508.4" textLength="817.4" clip-path="url(#breeze-ci-image-build-line-61)">Additional environment variables  [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="1532.8" textLength="1464" clip-path="url(#breeze-ci-image-build-line-62)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-ci-image-build-r1" x="1464" y="1532.8" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-62)">
-</text><text class="breeze-ci-image-build-r5" x="0" y="1557.2" textLength="24.4" clip-path="url(#breeze-ci-image-build-line-63)">â•─</text><text class="breeze-ci-image-build-r5" x="24.4" y="1557.2" textLength="622.2" clip-path="url(#breeze-ci-image-build-line-63)"> Preparing cache and push (for maintainers and CI) </text><text class="breeze-ci-image-build-r5" x="646.6" y="1557.2" textLength="793" clip-path="url(#breeze-ci-image-build-line-63)"> [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="1581.6" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-64)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1581.6" textLength="268.4" clip-path="url(#breeze-ci-image-build-line-64)">--builder             </text><text class="breeze-ci-image-build-r1" x="317.2" y="1581.6" textLength="768.6" clip-path="url(#breeze-ci-image-build-line-64)">Buildx&# [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="1606" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-65)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1606" textLength="268.4" clip-path="url(#breeze-ci-image-build-line-65)">--platform            </text><text class="breeze-ci-image-build-r1" x="317.2" y="1606" textLength="341.6" clip-path="url(#breeze-ci-image-build-line-65)">Platform for&# [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="1630.4" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-66)">│</text><text class="breeze-ci-image-build-r7" x="317.2" y="1630.4" textLength="292.8" clip-path="url(#breeze-ci-image-build-line-66)">linux/amd64,linux/arm64)</text><text class="breeze-ci-image-build-r5" x="1451.8" y="1630.4" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-66)">│</text><text class="breeze-ci-image-build-r1" x="1464" y="1630.4" tex [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="1654.8" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-67)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1654.8" textLength="268.4" clip-path="url(#breeze-ci-image-build-line-67)">--push                </text><text class="breeze-ci-image-build-r1" x="317.2" y="1654.8" textLength="353.8" clip-path="url(#breeze-ci-image-build-lin [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="1679.2" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-68)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1679.2" textLength="268.4" clip-path="url(#breeze-ci-image-build-line-68)">--prepare-buildx-cache</text><text class="breeze-ci-image-build-r1" x="317.2" y="1679.2" textLength="1122.4" clip-path="url(#breeze-ci-image-build-line-68)">Prepares build cache (this is done as  [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="1703.6" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-69)">│</text><text class="breeze-ci-image-build-r1" x="317.2" y="1703.6" textLength="1122.4" clip-path="url(#breeze-ci-image-build-line-69)">image).                                     [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="1728" textLength="1464" clip-path="url(#breeze-ci-image-build-line-70)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-ci-image-build-r1" x="1464" y="1728" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-70)">
-</text><text class="breeze-ci-image-build-r5" x="0" y="1752.4" textLength="24.4" clip-path="url(#breeze-ci-image-build-line-71)">â•─</text><text class="breeze-ci-image-build-r5" x="24.4" y="1752.4" textLength="280.6" clip-path="url(#breeze-ci-image-build-line-71)"> GitHub authentication </text><text class="breeze-ci-image-build-r5" x="305" y="1752.4" textLength="1134.6" clip-path="url(#breeze-ci-image-build-line-71)">───────────────────────────────────────────────────────── [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="1776.8" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-72)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1776.8" textLength="231.8" clip-path="url(#breeze-ci-image-build-line-72)">--github-repository</text><text class="breeze-ci-image-build-r6" x="280.6" y="1776.8" textLength="24.4" clip-path="url(#breeze-ci-image-build-line-72)">-g</text><text class="breeze-ci-image-build-r1" x="329.4" y="1776.8" textLeng [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="1801.2" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-73)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1801.2" textLength="231.8" clip-path="url(#breeze-ci-image-build-line-73)">--github-token     </text><text class="breeze-ci-image-build-r1" x="329.4" y="1801.2" textLength="512.4" clip-path="url(#breeze-ci-image-build-line-73)">The token used to authenticate& [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="1825.6" textLength="1464" clip-path="url(#breeze-ci-image-build-line-74)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-ci-image-build-r1" x="1464" y="1825.6" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-74)">
-</text><text class="breeze-ci-image-build-r5" x="0" y="1850" textLength="24.4" clip-path="url(#breeze-ci-image-build-line-75)">â•─</text><text class="breeze-ci-image-build-r5" x="24.4" y="1850" textLength="195.2" clip-path="url(#breeze-ci-image-build-line-75)"> Common options </text><text class="breeze-ci-image-build-r5" x="219.6" y="1850" textLength="1220" clip-path="url(#breeze-ci-image-build-line-75)">────────────────────────────────────────────────────────────────────── [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="1874.4" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-76)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1874.4" textLength="109.8" clip-path="url(#breeze-ci-image-build-line-76)">--answer </text><text class="breeze-ci-image-build-r6" x="158.6" y="1874.4" textLength="24.4" clip-path="url(#breeze-ci-image-build-line-76)">-a</text><text class="breeze-ci-image-build-r1" x="207.4" y="1874.4" textLength="3 [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="1898.8" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-77)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1898.8" textLength="109.8" clip-path="url(#breeze-ci-image-build-line-77)">--dry-run</text><text class="breeze-ci-image-build-r6" x="158.6" y="1898.8" textLength="24.4" clip-path="url(#breeze-ci-image-build-line-77)">-D</text><text class="breeze-ci-image-build-r1" x="207.4" y="1898.8" textLength="719.8" [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="1923.2" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-78)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1923.2" textLength="109.8" clip-path="url(#breeze-ci-image-build-line-78)">--verbose</text><text class="breeze-ci-image-build-r6" x="158.6" y="1923.2" textLength="24.4" clip-path="url(#breeze-ci-image-build-line-78)">-v</text><text class="breeze-ci-image-build-r1" x="207.4" y="1923.2" textLength="585.6" [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="1947.6" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-79)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1947.6" textLength="109.8" clip-path="url(#breeze-ci-image-build-line-79)">--help   </text><text class="breeze-ci-image-build-r6" x="158.6" y="1947.6" textLength="24.4" clip-path="url(#breeze-ci-image-build-line-79)">-h</text><text class="breeze-ci-image-build-r1" x="207.4" y="1947.6" tex [...]
-</text><text class="breeze-ci-image-build-r5" x="0" y="1972" textLength="1464" clip-path="url(#breeze-ci-image-build-line-80)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-ci-image-build-r1" x="1464" y="1972" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-80)">
+</text><text class="breeze-ci-image-build-r5" x="0" y="971.6" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-39)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="971.6" textLength="366" clip-path="url(#breeze-ci-image-build-line-39)">--disable-airflow-repo-cache  </text><text class="breeze-ci-image-build-r1" x="414.8" y="971.6" textLength="658.8" clip-path="url(#breeze-ci-image-build-line-39)">Disable cache from Airflow repository [...]
+</text><text class="breeze-ci-image-build-r5" x="0" y="996" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-40)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="996" textLength="366" clip-path="url(#breeze-ci-image-build-line-40)">--install-mysql-client-type   </text><text class="breeze-ci-image-build-r1" x="414.8" y="996" textLength="488" clip-path="url(#breeze-ci-image-build-line-40)">Which client to choose when installing [...]
+</text><text class="breeze-ci-image-build-r5" x="0" y="1020.4" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-41)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1020.4" textLength="366" clip-path="url(#breeze-ci-image-build-line-41)">--python-image                </text><text class="breeze-ci-image-build-r1" x="414.8" y="1020.4" textLength="1024.8" clip-path="url(#breeze-ci-image-bu [...]
+</text><text class="breeze-ci-image-build-r5" x="0" y="1044.8" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-42)">│</text><text class="breeze-ci-image-build-r1" x="414.8" y="1044.8" textLength="561.2" clip-path="url(#breeze-ci-image-build-line-42)">something like: python:VERSION-slim-bookworm. </text><text class="breeze-ci-image-build-r7" x="976" y="1044.8" textLength="73.2" clip-path="url(#breeze-ci-image-build-line-42)">(TEXT)</text><text class="breeze-ci- [...]
+</text><text class="breeze-ci-image-build-r5" x="0" y="1069.2" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-43)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1069.2" textLength="97.6" clip-path="url(#breeze-ci-image-build-line-43)">--use-uv</text><text class="breeze-ci-image-build-r1" x="122" y="1069.2" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-43)">/</text><text class="breeze-ci-image-build-r4" x="134.2" y="1069.2" textLength="134.2" clip [...]
+</text><text class="breeze-ci-image-build-r5" x="0" y="1093.6" textLength="1464" clip-path="url(#breeze-ci-image-build-line-44)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-ci-image-build-r1" x="1464" y="1093.6" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-44)">
+</text><text class="breeze-ci-image-build-r5" x="0" y="1118" textLength="24.4" clip-path="url(#breeze-ci-image-build-line-45)">â•─</text><text class="breeze-ci-image-build-r5" x="24.4" y="1118" textLength="597.8" clip-path="url(#breeze-ci-image-build-line-45)"> Selecting constraint location (for power users) </text><text class="breeze-ci-image-build-r5" x="622.2" y="1118" textLength="817.4" clip-path="url(#breeze-ci-image-build-line-45)">──────────────── [...]
+</text><text class="breeze-ci-image-build-r5" x="0" y="1142.4" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-46)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1142.4" textLength="378.2" clip-path="url(#breeze-ci-image-build-line-46)">--airflow-constraints-location </text><text class="breeze-ci-image-build-r1" x="427" y="1142.4" textLength="915" clip-path="url(#breeze-ci-image-build-line-46)">Location of airflow constraints to use [...]
+</text><text class="breeze-ci-image-build-r5" x="0" y="1166.8" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-47)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1166.8" textLength="378.2" clip-path="url(#breeze-ci-image-build-line-47)">--airflow-constraints-mode     </text><text class="breeze-ci-image-build-r1" x="427" y="1166.8" textLength="671" clip-path="url(#breeze-ci-image-build-line-47)">Mode of constraints for  [...]
+</text><text class="breeze-ci-image-build-r5" x="0" y="1191.2" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-48)">│</text><text class="breeze-ci-image-build-r5" x="427" y="1191.2" textLength="353.8" clip-path="url(#breeze-ci-image-build-line-48)">constraints-source-providers]</text><text class="breeze-ci-image-build-r7" x="793" y="1191.2" textLength="561.2" clip-path="url(#breeze-ci-image-build-line-48)">(constraints-source-providers | constraints | </t [...]
+</text><text class="breeze-ci-image-build-r5" x="0" y="1215.6" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-49)">│</text><text class="breeze-ci-image-build-r7" x="427" y="1215.6" textLength="305" clip-path="url(#breeze-ci-image-build-line-49)">constraints-no-providers)</text><text class="breeze-ci-image-build-r5" x="1451.8" y="1215.6" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-49)">│</text><text class="breeze-ci-image-build-r1" x="1464" y="1215.6" textLe [...]
+</text><text class="breeze-ci-image-build-r5" x="0" y="1240" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-50)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1240" textLength="378.2" clip-path="url(#breeze-ci-image-build-line-50)">--airflow-constraints-reference</text><text class="breeze-ci-image-build-r1" x="427" y="1240" textLength="646.6" clip-path="url(#breeze-ci-image-build-line-50)">Constraint reference to use when building  [...]
+</text><text class="breeze-ci-image-build-r5" x="0" y="1264.4" textLength="1464" clip-path="url(#breeze-ci-image-build-line-51)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-ci-image-build-r1" x="1464" y="1264.4" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-51)">
+</text><text class="breeze-ci-image-build-r5" x="0" y="1288.8" textLength="24.4" clip-path="url(#breeze-ci-image-build-line-52)">â•─</text><text class="breeze-ci-image-build-r5" x="24.4" y="1288.8" textLength="634.4" clip-path="url(#breeze-ci-image-build-line-52)"> Choosing dependencies and extras (for power users) </text><text class="breeze-ci-image-build-r5" x="658.8" y="1288.8" textLength="780.8" clip-path="url(#breeze-ci-image-build-line-52)">── [...]
+</text><text class="breeze-ci-image-build-r5" x="0" y="1313.2" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-53)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1313.2" textLength="341.6" clip-path="url(#breeze-ci-image-build-line-53)">--additional-airflow-extras </text><text class="breeze-ci-image-build-r1" x="390.4" y="1313.2" textLength="780.8" clip-path="url(#breeze-ci-image-build-line-53)">Additional extra package while installing& [...]
+</text><text class="breeze-ci-image-build-r5" x="0" y="1337.6" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-54)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1337.6" textLength="341.6" clip-path="url(#breeze-ci-image-build-line-54)">--additional-python-deps    </text><text class="breeze-ci-image-build-r1" x="390.4" y="1337.6" textLength="780.8" clip-path="url(#breeze-ci-image-build-line-54)">Additional python dependencies t [...]
+</text><text class="breeze-ci-image-build-r5" x="0" y="1362" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-55)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1362" textLength="341.6" clip-path="url(#breeze-ci-image-build-line-55)">--dev-apt-deps              </text><text class="breeze-ci-image-build-r1" x="390.4" y="1362" textLength="658.8" clip-path="url(#breeze-ci-image-build-line-55)">Apt [...]
+</text><text class="breeze-ci-image-build-r5" x="0" y="1386.4" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-56)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1386.4" textLength="341.6" clip-path="url(#breeze-ci-image-build-line-56)">--additional-dev-apt-deps   </text><text class="breeze-ci-image-build-r1" x="390.4" y="1386.4" textLength="793" clip-path="url(#breeze-ci-image-build-line-56)">Additional apt dev dependencies to [...]
+</text><text class="breeze-ci-image-build-r5" x="0" y="1410.8" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-57)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1410.8" textLength="341.6" clip-path="url(#breeze-ci-image-build-line-57)">--dev-apt-command           </text><text class="breeze-ci-image-build-r1" x="390.4" y="1410.8" textLength="634.4" clip-path="url(#breeze-ci-image-build-line-57)">Command  [...]
+</text><text class="breeze-ci-image-build-r5" x="0" y="1435.2" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-58)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1435.2" textLength="341.6" clip-path="url(#breeze-ci-image-build-line-58)">--additional-dev-apt-command</text><text class="breeze-ci-image-build-r1" x="390.4" y="1435.2" textLength="768.6" clip-path="url(#breeze-ci-image-build-line-58)">Additional command executed before dev apt [...]
+</text><text class="breeze-ci-image-build-r5" x="0" y="1459.6" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-59)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1459.6" textLength="341.6" clip-path="url(#breeze-ci-image-build-line-59)">--additional-dev-apt-env    </text><text class="breeze-ci-image-build-r1" x="390.4" y="1459.6" textLength="817.4" clip-path="url(#breeze-ci-image-build-line-59)">Additional environment variables  [...]
+</text><text class="breeze-ci-image-build-r5" x="0" y="1484" textLength="1464" clip-path="url(#breeze-ci-image-build-line-60)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-ci-image-build-r1" x="1464" y="1484" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-60)">
+</text><text class="breeze-ci-image-build-r5" x="0" y="1508.4" textLength="24.4" clip-path="url(#breeze-ci-image-build-line-61)">â•─</text><text class="breeze-ci-image-build-r5" x="24.4" y="1508.4" textLength="622.2" clip-path="url(#breeze-ci-image-build-line-61)"> Preparing cache and push (for maintainers and CI) </text><text class="breeze-ci-image-build-r5" x="646.6" y="1508.4" textLength="793" clip-path="url(#breeze-ci-image-build-line-61)"> [...]
+</text><text class="breeze-ci-image-build-r5" x="0" y="1532.8" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-62)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1532.8" textLength="268.4" clip-path="url(#breeze-ci-image-build-line-62)">--builder             </text><text class="breeze-ci-image-build-r1" x="317.2" y="1532.8" textLength="768.6" clip-path="url(#breeze-ci-image-build-line-62)">Buildx&# [...]
+</text><text class="breeze-ci-image-build-r5" x="0" y="1557.2" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-63)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1557.2" textLength="268.4" clip-path="url(#breeze-ci-image-build-line-63)">--platform            </text><text class="breeze-ci-image-build-r1" x="317.2" y="1557.2" textLength="341.6" clip-path="url(#breeze-ci-image-build-line-63)">Platform  [...]
+</text><text class="breeze-ci-image-build-r5" x="0" y="1581.6" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-64)">│</text><text class="breeze-ci-image-build-r7" x="317.2" y="1581.6" textLength="292.8" clip-path="url(#breeze-ci-image-build-line-64)">linux/amd64,linux/arm64)</text><text class="breeze-ci-image-build-r5" x="1451.8" y="1581.6" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-64)">│</text><text class="breeze-ci-image-build-r1" x="1464" y="1581.6" tex [...]
+</text><text class="breeze-ci-image-build-r5" x="0" y="1606" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-65)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1606" textLength="268.4" clip-path="url(#breeze-ci-image-build-line-65)">--push                </text><text class="breeze-ci-image-build-r1" x="317.2" y="1606" textLength="353.8" clip-path="url(#breeze-ci-image-build-line-65)" [...]
+</text><text class="breeze-ci-image-build-r5" x="0" y="1630.4" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-66)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1630.4" textLength="268.4" clip-path="url(#breeze-ci-image-build-line-66)">--prepare-buildx-cache</text><text class="breeze-ci-image-build-r1" x="317.2" y="1630.4" textLength="1122.4" clip-path="url(#breeze-ci-image-build-line-66)">Prepares build cache (this is done as  [...]
+</text><text class="breeze-ci-image-build-r5" x="0" y="1654.8" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-67)">│</text><text class="breeze-ci-image-build-r1" x="317.2" y="1654.8" textLength="1122.4" clip-path="url(#breeze-ci-image-build-line-67)">image).                                     [...]
+</text><text class="breeze-ci-image-build-r5" x="0" y="1679.2" textLength="1464" clip-path="url(#breeze-ci-image-build-line-68)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-ci-image-build-r1" x="1464" y="1679.2" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-68)">
+</text><text class="breeze-ci-image-build-r5" x="0" y="1703.6" textLength="24.4" clip-path="url(#breeze-ci-image-build-line-69)">â•─</text><text class="breeze-ci-image-build-r5" x="24.4" y="1703.6" textLength="280.6" clip-path="url(#breeze-ci-image-build-line-69)"> GitHub authentication </text><text class="breeze-ci-image-build-r5" x="305" y="1703.6" textLength="1134.6" clip-path="url(#breeze-ci-image-build-line-69)">───────────────────────────────────────────────────────── [...]
+</text><text class="breeze-ci-image-build-r5" x="0" y="1728" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-70)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1728" textLength="231.8" clip-path="url(#breeze-ci-image-build-line-70)">--github-repository</text><text class="breeze-ci-image-build-r6" x="280.6" y="1728" textLength="24.4" clip-path="url(#breeze-ci-image-build-line-70)">-g</text><text class="breeze-ci-image-build-r1" x="329.4" y="1728" textLength="597. [...]
+</text><text class="breeze-ci-image-build-r5" x="0" y="1752.4" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-71)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1752.4" textLength="231.8" clip-path="url(#breeze-ci-image-build-line-71)">--github-token     </text><text class="breeze-ci-image-build-r1" x="329.4" y="1752.4" textLength="512.4" clip-path="url(#breeze-ci-image-build-line-71)">The token used to authenticate& [...]
+</text><text class="breeze-ci-image-build-r5" x="0" y="1776.8" textLength="1464" clip-path="url(#breeze-ci-image-build-line-72)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-ci-image-build-r1" x="1464" y="1776.8" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-72)">
+</text><text class="breeze-ci-image-build-r5" x="0" y="1801.2" textLength="24.4" clip-path="url(#breeze-ci-image-build-line-73)">â•─</text><text class="breeze-ci-image-build-r5" x="24.4" y="1801.2" textLength="195.2" clip-path="url(#breeze-ci-image-build-line-73)"> Common options </text><text class="breeze-ci-image-build-r5" x="219.6" y="1801.2" textLength="1220" clip-path="url(#breeze-ci-image-build-line-73)">──────────────────────────────────────────────────────────────── [...]
+</text><text class="breeze-ci-image-build-r5" x="0" y="1825.6" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-74)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1825.6" textLength="109.8" clip-path="url(#breeze-ci-image-build-line-74)">--answer </text><text class="breeze-ci-image-build-r6" x="158.6" y="1825.6" textLength="24.4" clip-path="url(#breeze-ci-image-build-line-74)">-a</text><text class="breeze-ci-image-build-r1" x="207.4" y="1825.6" textLength="3 [...]
+</text><text class="breeze-ci-image-build-r5" x="0" y="1850" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-75)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1850" textLength="109.8" clip-path="url(#breeze-ci-image-build-line-75)">--dry-run</text><text class="breeze-ci-image-build-r6" x="158.6" y="1850" textLength="24.4" clip-path="url(#breeze-ci-image-build-line-75)">-D</text><text class="breeze-ci-image-build-r1" x="207.4" y="1850" textLength="719.8" clip-pa [...]
+</text><text class="breeze-ci-image-build-r5" x="0" y="1874.4" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-76)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1874.4" textLength="109.8" clip-path="url(#breeze-ci-image-build-line-76)">--verbose</text><text class="breeze-ci-image-build-r6" x="158.6" y="1874.4" textLength="24.4" clip-path="url(#breeze-ci-image-build-line-76)">-v</text><text class="breeze-ci-image-build-r1" x="207.4" y="1874.4" textLength="585.6" [...]
+</text><text class="breeze-ci-image-build-r5" x="0" y="1898.8" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-77)">│</text><text class="breeze-ci-image-build-r4" x="24.4" y="1898.8" textLength="109.8" clip-path="url(#breeze-ci-image-build-line-77)">--help   </text><text class="breeze-ci-image-build-r6" x="158.6" y="1898.8" textLength="24.4" clip-path="url(#breeze-ci-image-build-line-77)">-h</text><text class="breeze-ci-image-build-r1" x="207.4" y="1898.8" tex [...]
+</text><text class="breeze-ci-image-build-r5" x="0" y="1923.2" textLength="1464" clip-path="url(#breeze-ci-image-build-line-78)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-ci-image-build-r1" x="1464" y="1923.2" textLength="12.2" clip-path="url(#breeze-ci-image-build-line-78)">
</text>
</g>
</g>
diff --git a/dev/breeze/doc/images/output_ci-image_build.txt b/dev/breeze/doc/images/output_ci-image_build.txt
index 164289979d7..99d1be2ffd4 100644
--- a/dev/breeze/doc/images/output_ci-image_build.txt
+++ b/dev/breeze/doc/images/output_ci-image_build.txt
@@ -1 +1 @@
-a0a5487301ae9980a7728b910f28064c
+52c75916e9ed2356fcf749cb9003f5e8
diff --git a/dev/breeze/doc/images/output_prod-image_build.svg b/dev/breeze/doc/images/output_prod-image_build.svg
index cedd81db563..96f173089d6 100644
--- a/dev/breeze/doc/images/output_prod-image_build.svg
+++ b/dev/breeze/doc/images/output_prod-image_build.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 1482 2612.0" xmlns="http://www.w3.org/2000/svg">
+<svg class="rich-terminal" viewBox="0 0 1482 2685.2" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@@ -43,7 +43,7 @@
<defs>
<clipPath id="breeze-prod-image-build-clip-terminal">
- <rect x="0" y="0" width="1463.0" height="2561.0" />
+ <rect x="0" y="0" width="1463.0" height="2634.2" />
</clipPath>
<clipPath id="breeze-prod-image-build-line-0">
<rect x="0" y="1.5" width="1464" height="24.65"/>
@@ -357,9 +357,18 @@
<clipPath id="breeze-prod-image-build-line-103">
<rect x="0" y="2514.7" width="1464" height="24.65"/>
</clipPath>
+<clipPath id="breeze-prod-image-build-line-104">
+ <rect x="0" y="2539.1" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-prod-image-build-line-105">
+ <rect x="0" y="2563.5" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-prod-image-build-line-106">
+ <rect x="0" y="2587.9" width="1464" height="24.65"/>
+ </clipPath>
</defs>
- <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1480" height="2610" rx="8"/><text class="breeze-prod-image-build-title" fill="#c5c8c6" text-anchor="middle" x="740" y="27">Command: prod-image build</text>
+ <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1480" height="2683.2" rx="8"/><text class="breeze-prod-image-build-title" fill="#c5c8c6" text-anchor="middle" x="740" y="27">Command: prod-image build</text>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
@@ -401,79 +410,82 @@
</text><text class="breeze-prod-image-build-r5" x="0" y="727.6" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-29)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="727.6" textLength="366" clip-path="url(#breeze-prod-image-build-line-29)">--additional-pip-install-flags</text><text class="breeze-prod-image-build-r1" x="414.8" y="727.6" textLength="1024.8" clip-path="url(#breeze-prod-image-build-line-29)">Additional flags added to `pip i [...]
</text><text class="breeze-prod-image-build-r5" x="0" y="752" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-30)">│</text><text class="breeze-prod-image-build-r6" x="414.8" y="752" textLength="73.2" clip-path="url(#breeze-prod-image-build-line-30)">(TEXT)</text><text class="breeze-prod-image-build-r5" x="1451.8" y="752" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-30)">│</text><text class="breeze-prod-image-build-r1" x="1464" y="752" textLength="12.2" cl [...]
</text><text class="breeze-prod-image-build-r5" x="0" y="776.4" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-31)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="776.4" textLength="366" clip-path="url(#breeze-prod-image-build-line-31)">--commit-sha                  </text><text class="breeze-prod-image-build-r1" x="414.8" y="776.4" textLength="549" clip-path="url(#bree [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="800.8" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-32)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="800.8" textLength="366" clip-path="url(#breeze-prod-image-build-line-32)">--debian-version              </text><text class="breeze-prod-image-build-r1" x="414.8" y="800.8" textLength="805.2" clip-path="url(#breeze-prod-image-buil [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="825.2" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-33)">│</text><text class="breeze-prod-image-build-r5" x="414.8" y="825.2" textLength="109.8" clip-path="url(#breeze-prod-image-build-line-33)">bookworm]</text><text class="breeze-prod-image-build-r6" x="536.8" y="825.2" textLength="122" clip-path="url(#breeze-prod-image-build-line-33)">(bookworm)</text><text class="breeze-prod-image-build-r5" x="1451.8" y="825.2" [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="849.6" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-34)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="849.6" textLength="366" clip-path="url(#breeze-prod-image-build-line-34)">--python-image                </text><text class="breeze-prod-image-build-r1" x="414.8" y="849.6" textLength="1024.8" clip-path="url(#breeze-prod [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="874" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-35)">│</text><text class="breeze-prod-image-build-r1" x="414.8" y="874" textLength="561.2" clip-path="url(#breeze-prod-image-build-line-35)">something like: python:VERSION-slim-bookworm. </text><text class="breeze-prod-image-build-r6" x="976" y="874" textLength="73.2" clip-path="url(#breeze-prod-image-build-line-35)">(TEXT)</text><text class="breeze- [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="898.4" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-36)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="898.4" textLength="366" clip-path="url(#breeze-prod-image-build-line-36)">--skip-asset-compiled-check   </text><text class="breeze-prod-image-build-r1" x="414.8" y="898.4" textLength="951.6" clip-path="url(#breeze-prod-image-build-line-36)">Skip asset compilation check [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="922.8" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-37)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="922.8" textLength="97.6" clip-path="url(#breeze-prod-image-build-line-37)">--use-uv</text><text class="breeze-prod-image-build-r1" x="122" y="922.8" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-37)">/</text><text class="breeze-prod-image-build-r4" x="134.2" y="922.8" textLength="1 [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="947.2" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-38)">│</text><text class="breeze-prod-image-build-r1" x="414.8" y="947.2" textLength="1024.8" clip-path="url(#breeze-prod-image-build-line-38)">installing from sources and False for installing from packages).                    [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="971.6" textLength="1464" clip-path="url(#breeze-prod-image-build-line-39)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-prod-image-build-r1" x="1464" y="971.6" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-39)">
-</text><text class="breeze-prod-image-build-r5" x="0" y="996" textLength="24.4" clip-path="url(#breeze-prod-image-build-line-40)">â•─</text><text class="breeze-prod-image-build-r5" x="24.4" y="996" textLength="597.8" clip-path="url(#breeze-prod-image-build-line-40)"> Selecting constraint location (for power users) </text><text class="breeze-prod-image-build-r5" x="622.2" y="996" textLength="817.4" clip-path="url(#breeze-prod-image-build-line-40)">─────── [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1020.4" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-41)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1020.4" textLength="695.4" clip-path="url(#breeze-prod-image-build-line-41)">--airflow-constraints-location                           </text><text class="breeze-pr [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1044.8" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-42)">│</text><text class="breeze-prod-image-build-r1" x="744.2" y="1044.8" textLength="256.2" clip-path="url(#breeze-prod-image-build-line-42)">local context file). </text><text class="breeze-prod-image-build-r6" x="1000.4" y="1044.8" textLength="73.2" clip-path="url(#breeze-prod-image-build-line-42)">(TEXT)</text><text class="breeze-prod-image-bu [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1069.2" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-43)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1069.2" textLength="695.4" clip-path="url(#breeze-prod-image-build-line-43)">--airflow-constraints-mode                               </text><t [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1093.6" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-44)">│</text><text class="breeze-prod-image-build-r5" x="744.2" y="1093.6" textLength="268.4" clip-path="url(#breeze-prod-image-build-line-44)">[default: constraints]</text><text class="breeze-prod-image-build-r6" x="1024.8" y="1093.6" textLength="183" clip-path="url(#breeze-prod-image-build-line-44)">(constraints | </text><text class="breeze-prod [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1118" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-45)">│</text><text class="breeze-prod-image-build-r6" x="744.2" y="1118" textLength="683.2" clip-path="url(#breeze-prod-image-build-line-45)">constraints-no-providers | constraints-source-providers)</text><text class="breeze-prod-image-build-r5" x="1451.8" y="1118" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-45)">│</text><text class="b [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1142.4" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-46)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1142.4" textLength="695.4" clip-path="url(#breeze-prod-image-build-line-46)">--airflow-constraints-reference                          </text><text class="breeze-prod-im [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1166.8" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-47)">│</text><text class="breeze-prod-image-build-r6" x="744.2" y="1166.8" textLength="73.2" clip-path="url(#breeze-prod-image-build-line-47)">(TEXT)</text><text class="breeze-prod-image-build-r5" x="1451.8" y="1166.8" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-47)">│</text><text class="breeze-prod-image-build-r1" x="1464" y="1166.8" textLeng [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1191.2" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-48)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1191.2" textLength="561.2" clip-path="url(#breeze-prod-image-build-line-48)">--airflow-fallback-no-constraints-installation</text><text class="breeze-prod-image-build-r1" x="585.6" y="1191.2" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-48)">/</text><text class="breeze-prod-image [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1215.6" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-49)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1215.6" textLength="475.8" clip-path="url(#breeze-prod-image-build-line-49)">ow-fallback-no-constraints-installation</text><text class="breeze-prod-image-build-r1" x="744.2" y="1215.6" textLength="244" clip-path="url(#breeze-prod-image-build-line-49)">installation fails. </text><text cla [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1240" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-50)">│</text><text class="breeze-prod-image-build-r5" x="744.2" y="1240" textLength="549" clip-path="url(#breeze-prod-image-build-line-50)">airflow-fallback-no-constraints-installation]</text><text class="breeze-prod-image-build-r5" x="1451.8" y="1240" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-50)">│</text><text class="breeze-prod-image-build- [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1264.4" textLength="1464" clip-path="url(#breeze-prod-image-build-line-51)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-prod-image-build-r1" x="1464" y="1264.4" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-51)">
-</text><text class="breeze-prod-image-build-r5" x="0" y="1288.8" textLength="24.4" clip-path="url(#breeze-prod-image-build-line-52)">â•─</text><text class="breeze-prod-image-build-r5" x="24.4" y="1288.8" textLength="634.4" clip-path="url(#breeze-prod-image-build-line-52)"> Choosing dependencies and extras (for power users) </text><text class="breeze-prod-image-build-r5" x="658.8" y="1288.8" textLength="780.8" clip-path="url(#breeze-prod-image-build- [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1313.2" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-53)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1313.2" textLength="390.4" clip-path="url(#breeze-prod-image-build-line-53)">--airflow-extras                </text><text class="breeze-prod-image-build-r1" x="439.2" y="1313.2" textLength="366" clip-path="url(#breeze- [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1337.6" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-54)">│</text><text class="breeze-prod-image-build-r5" x="439.2" y="1337.6" textLength="1000.4" clip-path="url(#breeze-prod-image-build-line-54)">aiobotocore,amazon,async,celery,cncf-kubernetes,common-io,common-messaging,docker,</text><text class="breeze-prod-image-build-r5" x="1451.8" y="1337.6" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-54)" [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1362" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-55)">│</text><text class="breeze-prod-image-build-r5" x="439.2" y="1362" textLength="1000.4" clip-path="url(#breeze-prod-image-build-line-55)">elasticsearch,fab,ftp,git,google,google-auth,graphviz,grpc,hashicorp,http,ldap,mic</text><text class="breeze-prod-image-build-r5" x="1451.8" y="1362" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-55)">│</te [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1386.4" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-56)">│</text><text class="breeze-prod-image-build-r5" x="439.2" y="1386.4" textLength="1000.4" clip-path="url(#breeze-prod-image-build-line-56)">rosoft-azure,mysql,odbc,openlineage,opensearch,pandas,postgres,redis,sendgrid,sftp</text><text class="breeze-prod-image-build-r5" x="1451.8" y="1386.4" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-56)" [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1410.8" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-57)">│</text><text class="breeze-prod-image-build-r5" x="439.2" y="1410.8" textLength="378.2" clip-path="url(#breeze-prod-image-build-line-57)">,slack,snowflake,ssh,statsd,uv]</text><text class="breeze-prod-image-build-r6" x="829.6" y="1410.8" textLength="73.2" clip-path="url(#breeze-prod-image-build-line-57)">(TEXT)</text><text class="breeze-prod-image-build-r5 [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1435.2" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-58)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1435.2" textLength="390.4" clip-path="url(#breeze-prod-image-build-line-58)">--additional-airflow-extras     </text><text class="breeze-prod-image-build-r1" x="439.2" y="1435.2" textLength="780.8" clip-path="url(#breeze-prod-image-build-line-58)">Additional extra  [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1459.6" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-59)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1459.6" textLength="390.4" clip-path="url(#breeze-prod-image-build-line-59)">--additional-python-deps        </text><text class="breeze-prod-image-build-r1" x="439.2" y="1459.6" textLength="780.8" clip-path="url(#breeze-prod-image-build-line-59)">Additional& [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1484" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-60)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1484" textLength="390.4" clip-path="url(#breeze-prod-image-build-line-60)">--dev-apt-deps                  </text><text class="breeze-prod-image-build-r1" x="439.2" y="1484" textLength="658.8" clip-path="url(#b [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1508.4" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-61)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1508.4" textLength="390.4" clip-path="url(#breeze-prod-image-build-line-61)">--additional-dev-apt-deps       </text><text class="breeze-prod-image-build-r1" x="439.2" y="1508.4" textLength="793" clip-path="url(#breeze-prod-image-build-line-61)">Additional ap [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1532.8" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-62)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1532.8" textLength="390.4" clip-path="url(#breeze-prod-image-build-line-62)">--dev-apt-command               </text><text class="breeze-prod-image-build-r1" x="439.2" y="1532.8" textLength="634.4" clip-path="url(#breeze-pro [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1557.2" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-63)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1557.2" textLength="390.4" clip-path="url(#breeze-prod-image-build-line-63)">--additional-dev-apt-command    </text><text class="breeze-prod-image-build-r1" x="439.2" y="1557.2" textLength="768.6" clip-path="url(#breeze-prod-image-build-line-63)">Additional command ex [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1581.6" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-64)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1581.6" textLength="390.4" clip-path="url(#breeze-prod-image-build-line-64)">--additional-dev-apt-env        </text><text class="breeze-prod-image-build-r1" x="439.2" y="1581.6" textLength="817.4" clip-path="url(#breeze-prod-image-build-line-64)">Additional& [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1606" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-65)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1606" textLength="390.4" clip-path="url(#breeze-prod-image-build-line-65)">--runtime-apt-deps              </text><text class="breeze-prod-image-build-r1" x="439.2" y="1606" textLength="707.6" clip-path="url(#breeze-prod-image-bui [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1630.4" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-66)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1630.4" textLength="390.4" clip-path="url(#breeze-prod-image-build-line-66)">--additional-runtime-apt-deps   </text><text class="breeze-prod-image-build-r1" x="439.2" y="1630.4" textLength="841.8" clip-path="url(#breeze-prod-image-build-line-66)">Additional apt runtime [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1654.8" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-67)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1654.8" textLength="390.4" clip-path="url(#breeze-prod-image-build-line-67)">--runtime-apt-command           </text><text class="breeze-prod-image-build-r1" x="439.2" y="1654.8" textLength="683.2" clip-path="url(#breeze-prod-image-build-line-6 [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1679.2" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-68)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1679.2" textLength="390.4" clip-path="url(#breeze-prod-image-build-line-68)">--additional-runtime-apt-command</text><text class="breeze-prod-image-build-r1" x="439.2" y="1679.2" textLength="817.4" clip-path="url(#breeze-prod-image-build-line-68)">Additional command executed before&# [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1703.6" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-69)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1703.6" textLength="390.4" clip-path="url(#breeze-prod-image-build-line-69)">--additional-runtime-apt-env    </text><text class="breeze-prod-image-build-r1" x="439.2" y="1703.6" textLength="866.2" clip-path="url(#breeze-prod-image-build-line-69)">Additional environment [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1728" textLength="1464" clip-path="url(#breeze-prod-image-build-line-70)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-prod-image-build-r1" x="1464" y="1728" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-70)">
-</text><text class="breeze-prod-image-build-r5" x="0" y="1752.4" textLength="24.4" clip-path="url(#breeze-prod-image-build-line-71)">â•─</text><text class="breeze-prod-image-build-r5" x="24.4" y="1752.4" textLength="817.4" clip-path="url(#breeze-prod-image-build-line-71)"> Advanced customization options (for specific customization needs) </text><text class="breeze-prod-image-build-r5" x="841.8" y="1752.4" textLength="597.8" clip-path="url(#breeze-pr [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1776.8" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-72)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1776.8" textLength="524.6" clip-path="url(#breeze-prod-image-build-line-72)">--installation-method                      </text><text class="breeze-prod-image-build-r1" x="573.4" y="1776.8" [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1801.2" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-73)">│</text><text class="breeze-prod-image-build-r5" x="573.4" y="1801.2" textLength="146.4" clip-path="url(#breeze-prod-image-build-line-73)">[default: .]</text><text class="breeze-prod-image-build-r6" x="732" y="1801.2" textLength="244" clip-path="url(#breeze-prod-image-build-line-73)">(. | apache-airflow)</text><text class="breeze-prod-image-b [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1825.6" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-74)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1825.6" textLength="524.6" clip-path="url(#breeze-prod-image-build-line-74)">--install-airflow-reference                </text><text class="breeze-prod-image-build-r1" x="573.4" y="1825.6" textLength="536.8" clip-path= [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1850" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-75)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1850" textLength="524.6" clip-path="url(#breeze-prod-image-build-line-75)">--install-distributions-from-context       </text><text class="breeze-prod-image-build-r1" x="573.4" y="1850" textLength="866.2" clip-path="url(#breeze-prod-image-build-line-75)">Install [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1874.4" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-76)">│</text><text class="breeze-prod-image-build-r1" x="573.4" y="1874.4" textLength="183" clip-path="url(#breeze-prod-image-build-line-76)">image. Implies </text><text class="breeze-prod-image-build-r4" x="756.4" y="1874.4" textLength="341.6" clip-path="url(#breeze-prod-image-build-line-76)">--disable-airflow-repo-cache</text><text class="breeze-prod [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1898.8" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-77)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1898.8" textLength="524.6" clip-path="url(#breeze-prod-image-build-line-77)">--install-mysql-client-type                </text><text class="breeze-prod-image-build-r1" x="573.4" y="1898.8" textLength="488" clip-path="u [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1923.2" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-78)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1923.2" textLength="524.6" clip-path="url(#breeze-prod-image-build-line-78)">--cleanup-context                          </text><text class="breeze-prod-image-build-r1" [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1947.6" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-79)">│</text><text class="breeze-prod-image-build-r1" x="573.4" y="1947.6" textLength="170.8" clip-path="url(#breeze-prod-image-build-line-79)">together with </text><text class="breeze-prod-image-build-r4" x="744.2" y="1947.6" textLength="439.2" clip-path="url(#breeze-prod-image-build-line-79)">--install-distributions-from-context</text><text class="br [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1972" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-80)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1972" textLength="524.6" clip-path="url(#breeze-prod-image-build-line-80)">--use-constraints-for-context-distributions</text><text class="breeze-prod-image-build-r1" x="573.4" y="1972" textLength="866.2" clip-path="url(#breeze-prod-image-build-line-80)">Uses constraints for context [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="1996.4" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-81)">│</text><text class="breeze-prod-image-build-r1" x="573.4" y="1996.4" textLength="866.2" clip-path="url(#breeze-prod-image-build-line-81)">constraints store in docker-context-files or from github.              </text><text class="breeze-prod-image-build-r5" x [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="2020.8" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-82)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="2020.8" textLength="524.6" clip-path="url(#breeze-prod-image-build-line-82)">--disable-airflow-repo-cache               </text><text class="breeze-prod-image-build-r1" x="573.4" y="2020.8" textLength="658.8" clip-path="url( [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="2045.2" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-83)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="2045.2" textLength="524.6" clip-path="url(#breeze-prod-image-build-line-83)">--disable-mysql-client-installation        </text><text class="breeze-prod-image-build-r1" x="573.4" y="2045.2" textLength="341.6" clip-path="url(#breeze-prod-image-build-line-83)"> [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="2069.6" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-84)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="2069.6" textLength="524.6" clip-path="url(#breeze-prod-image-build-line-84)">--disable-mssql-client-installation        </text><text class="breeze-prod-image-build-r1" x="573.4" y="2069.6" textLength="341.6" clip-path="url(#breeze-prod-image-build-line-84)"> [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="2094" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-85)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="2094" textLength="524.6" clip-path="url(#breeze-prod-image-build-line-85)">--disable-postgres-client-installation     </text><text class="breeze-prod-image-build-r1" x="573.4" y="2094" textLength="378.2" clip-path="url(#breeze-prod-image-build-line-85)">Do not inst [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="2118.4" textLength="1464" clip-path="url(#breeze-prod-image-build-line-86)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-prod-image-build-r1" x="1464" y="2118.4" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-86)">
-</text><text class="breeze-prod-image-build-r5" x="0" y="2142.8" textLength="24.4" clip-path="url(#breeze-prod-image-build-line-87)">â•─</text><text class="breeze-prod-image-build-r5" x="24.4" y="2142.8" textLength="622.2" clip-path="url(#breeze-prod-image-build-line-87)"> Preparing cache and push (for maintainers and CI) </text><text class="breeze-prod-image-build-r5" x="646.6" y="2142.8" textLength="793" clip-path="url(#breeze-prod-image-buil [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="2167.2" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-88)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="2167.2" textLength="268.4" clip-path="url(#breeze-prod-image-build-line-88)">--builder             </text><text class="breeze-prod-image-build-r1" x="317.2" y="2167.2" textLength="768.6" clip-path="url(#breeze-prod-image-build-line-8 [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="2191.6" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-89)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="2191.6" textLength="268.4" clip-path="url(#breeze-prod-image-build-line-89)">--platform            </text><text class="breeze-prod-image-build-r1" x="317.2" y="2191.6" textLength="341.6" clip-path="url(#breeze-prod-image-build-line-89)">P [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="2216" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-90)">│</text><text class="breeze-prod-image-build-r6" x="317.2" y="2216" textLength="292.8" clip-path="url(#breeze-prod-image-build-line-90)">linux/amd64,linux/arm64)</text><text class="breeze-prod-image-build-r5" x="1451.8" y="2216" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-90)">│</text><text class="breeze-prod-image-build-r1" x="1464" y="221 [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="2240.4" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-91)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="2240.4" textLength="268.4" clip-path="url(#breeze-prod-image-build-line-91)">--push                </text><text class="breeze-prod-image-build-r1" x="317.2" y="2240.4" textLength="353.8" clip-path="url(#breeze-prod-ima [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="2264.8" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-92)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="2264.8" textLength="268.4" clip-path="url(#breeze-prod-image-build-line-92)">--prepare-buildx-cache</text><text class="breeze-prod-image-build-r1" x="317.2" y="2264.8" textLength="1122.4" clip-path="url(#breeze-prod-image-build-line-92)">Prepares build cache (this is done& [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="2289.2" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-93)">│</text><text class="breeze-prod-image-build-r1" x="317.2" y="2289.2" textLength="1122.4" clip-path="url(#breeze-prod-image-build-line-93)">image).                                   &# [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="2313.6" textLength="1464" clip-path="url(#breeze-prod-image-build-line-94)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-prod-image-build-r1" x="1464" y="2313.6" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-94)">
-</text><text class="breeze-prod-image-build-r5" x="0" y="2338" textLength="24.4" clip-path="url(#breeze-prod-image-build-line-95)">â•─</text><text class="breeze-prod-image-build-r5" x="24.4" y="2338" textLength="280.6" clip-path="url(#breeze-prod-image-build-line-95)"> GitHub authentication </text><text class="breeze-prod-image-build-r5" x="305" y="2338" textLength="1134.6" clip-path="url(#breeze-prod-image-build-line-95)">─────────────────────────────────────────────────── [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="2362.4" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-96)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="2362.4" textLength="231.8" clip-path="url(#breeze-prod-image-build-line-96)">--github-repository</text><text class="breeze-prod-image-build-r7" x="280.6" y="2362.4" textLength="24.4" clip-path="url(#breeze-prod-image-build-line-96)">-g</text><text class="breeze-prod-image-build-r1" x="329.4" y="23 [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="2386.8" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-97)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="2386.8" textLength="231.8" clip-path="url(#breeze-prod-image-build-line-97)">--github-token     </text><text class="breeze-prod-image-build-r1" x="329.4" y="2386.8" textLength="512.4" clip-path="url(#breeze-prod-image-build-line-97)">The token used to a [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="2411.2" textLength="1464" clip-path="url(#breeze-prod-image-build-line-98)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-prod-image-build-r1" x="1464" y="2411.2" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-98)">
-</text><text class="breeze-prod-image-build-r5" x="0" y="2435.6" textLength="24.4" clip-path="url(#breeze-prod-image-build-line-99)">â•─</text><text class="breeze-prod-image-build-r5" x="24.4" y="2435.6" textLength="195.2" clip-path="url(#breeze-prod-image-build-line-99)"> Common options </text><text class="breeze-prod-image-build-r5" x="219.6" y="2435.6" textLength="1220" clip-path="url(#breeze-prod-image-build-line-99)">──────────────────────────────────────────────────── [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="2460" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-100)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="2460" textLength="109.8" clip-path="url(#breeze-prod-image-build-line-100)">--answer </text><text class="breeze-prod-image-build-r7" x="158.6" y="2460" textLength="24.4" clip-path="url(#breeze-prod-image-build-line-100)">-a</text><text class="breeze-prod-image-build-r1" x="207.4" y="2460" text [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="2484.4" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-101)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="2484.4" textLength="109.8" clip-path="url(#breeze-prod-image-build-line-101)">--dry-run</text><text class="breeze-prod-image-build-r7" x="158.6" y="2484.4" textLength="24.4" clip-path="url(#breeze-prod-image-build-line-101)">-D</text><text class="breeze-prod-image-build-r1" x="207.4" y="2484.4" t [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="2508.8" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-102)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="2508.8" textLength="109.8" clip-path="url(#breeze-prod-image-build-line-102)">--verbose</text><text class="breeze-prod-image-build-r7" x="158.6" y="2508.8" textLength="24.4" clip-path="url(#breeze-prod-image-build-line-102)">-v</text><text class="breeze-prod-image-build-r1" x="207.4" y="2508.8" t [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="2533.2" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-103)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="2533.2" textLength="109.8" clip-path="url(#breeze-prod-image-build-line-103)">--help   </text><text class="breeze-prod-image-build-r7" x="158.6" y="2533.2" textLength="24.4" clip-path="url(#breeze-prod-image-build-line-103)">-h</text><text class="breeze-prod-image-build-r1" x="207. [...]
-</text><text class="breeze-prod-image-build-r5" x="0" y="2557.6" textLength="1464" clip-path="url(#breeze-prod-image-build-line-104)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-prod-image-build-r1" x="1464" y="2557.6" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-104)">
+</text><text class="breeze-prod-image-build-r5" x="0" y="800.8" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-32)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="800.8" textLength="366" clip-path="url(#breeze-prod-image-build-line-32)">--debian-version              </text><text class="breeze-prod-image-build-r1" x="414.8" y="800.8" textLength="805.2" clip-path="url(#breeze-prod-image-buil [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="825.2" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-33)">│</text><text class="breeze-prod-image-build-r6" x="414.8" y="825.2" textLength="231.8" clip-path="url(#breeze-prod-image-build-line-33)">(trixie | bookworm)</text><text class="breeze-prod-image-build-r5" x="1451.8" y="825.2" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-33)">│</text><text class="breeze-prod-image-build-r1" x="1464 [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="849.6" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-34)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="849.6" textLength="366" clip-path="url(#breeze-prod-image-build-line-34)">--image-flavor                </text><text class="breeze-prod-image-build-r1" x="414.8" y="849.6" textLength="1024.8" clip-path="url(#breeze-prod [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="874" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-35)">│</text><text class="breeze-prod-image-build-r1" x="414.8" y="874" textLength="829.6" clip-path="url(#breeze-prod-image-build-line-35)">'legacy' compiles Python from sources on a plain Debian base image. </text><text class="breeze-prod-image-build-r5" x="1244.4" y="874" textLength="122" clip-path [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="898.4" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-36)">│</text><text class="breeze-prod-image-build-r5" x="414.8" y="898.4" textLength="109.8" clip-path="url(#breeze-prod-image-build-line-36)">hardened]</text><text class="breeze-prod-image-build-r6" x="536.8" y="898.4" textLength="231.8" clip-path="url(#breeze-prod-image-build-line-36)">(hardened | legacy)</text><text class="breeze-prod-image-build-r5" [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="922.8" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-37)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="922.8" textLength="366" clip-path="url(#breeze-prod-image-build-line-37)">--python-image                </text><text class="breeze-prod-image-build-r1" x="414.8" y="922.8" textLength="1024.8" clip-path="url(#breeze-prod [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="947.2" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-38)">│</text><text class="breeze-prod-image-build-r1" x="414.8" y="947.2" textLength="561.2" clip-path="url(#breeze-prod-image-build-line-38)">something like: python:VERSION-slim-bookworm. </text><text class="breeze-prod-image-build-r6" x="976" y="947.2" textLength="73.2" clip-path="url(#breeze-prod-image-build-line-38)">(TEXT)</text><text class="b [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="971.6" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-39)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="971.6" textLength="366" clip-path="url(#breeze-prod-image-build-line-39)">--skip-asset-compiled-check   </text><text class="breeze-prod-image-build-r1" x="414.8" y="971.6" textLength="951.6" clip-path="url(#breeze-prod-image-build-line-39)">Skip asset compilation check [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="996" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-40)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="996" textLength="97.6" clip-path="url(#breeze-prod-image-build-line-40)">--use-uv</text><text class="breeze-prod-image-build-r1" x="122" y="996" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-40)">/</text><text class="breeze-prod-image-build-r4" x="134.2" y="996" textLength="134.2" cl [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1020.4" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-41)">│</text><text class="breeze-prod-image-build-r1" x="414.8" y="1020.4" textLength="1024.8" clip-path="url(#breeze-prod-image-build-line-41)">installing from sources and False for installing from packages).                   & [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1044.8" textLength="1464" clip-path="url(#breeze-prod-image-build-line-42)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-prod-image-build-r1" x="1464" y="1044.8" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-42)">
+</text><text class="breeze-prod-image-build-r5" x="0" y="1069.2" textLength="24.4" clip-path="url(#breeze-prod-image-build-line-43)">â•─</text><text class="breeze-prod-image-build-r5" x="24.4" y="1069.2" textLength="597.8" clip-path="url(#breeze-prod-image-build-line-43)"> Selecting constraint location (for power users) </text><text class="breeze-prod-image-build-r5" x="622.2" y="1069.2" textLength="817.4" clip-path="url(#breeze-prod-image-build-line-43) [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1093.6" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-44)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1093.6" textLength="695.4" clip-path="url(#breeze-prod-image-build-line-44)">--airflow-constraints-location                           </text><text class="breeze-pr [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1118" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-45)">│</text><text class="breeze-prod-image-build-r1" x="744.2" y="1118" textLength="256.2" clip-path="url(#breeze-prod-image-build-line-45)">local context file). </text><text class="breeze-prod-image-build-r6" x="1000.4" y="1118" textLength="73.2" clip-path="url(#breeze-prod-image-build-line-45)">(TEXT)</text><text class="breeze-prod-image-build-r5 [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1142.4" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-46)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1142.4" textLength="695.4" clip-path="url(#breeze-prod-image-build-line-46)">--airflow-constraints-mode                               </text><t [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1166.8" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-47)">│</text><text class="breeze-prod-image-build-r5" x="744.2" y="1166.8" textLength="268.4" clip-path="url(#breeze-prod-image-build-line-47)">[default: constraints]</text><text class="breeze-prod-image-build-r6" x="1024.8" y="1166.8" textLength="183" clip-path="url(#breeze-prod-image-build-line-47)">(constraints | </text><text class="breeze-prod [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1191.2" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-48)">│</text><text class="breeze-prod-image-build-r6" x="744.2" y="1191.2" textLength="683.2" clip-path="url(#breeze-prod-image-build-line-48)">constraints-no-providers | constraints-source-providers)</text><text class="breeze-prod-image-build-r5" x="1451.8" y="1191.2" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-48)">│</text><text cl [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1215.6" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-49)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1215.6" textLength="695.4" clip-path="url(#breeze-prod-image-build-line-49)">--airflow-constraints-reference                          </text><text class="breeze-prod-im [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1240" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-50)">│</text><text class="breeze-prod-image-build-r6" x="744.2" y="1240" textLength="73.2" clip-path="url(#breeze-prod-image-build-line-50)">(TEXT)</text><text class="breeze-prod-image-build-r5" x="1451.8" y="1240" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-50)">│</text><text class="breeze-prod-image-build-r1" x="1464" y="1240" textLength="12.2 [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1264.4" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-51)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1264.4" textLength="561.2" clip-path="url(#breeze-prod-image-build-line-51)">--airflow-fallback-no-constraints-installation</text><text class="breeze-prod-image-build-r1" x="585.6" y="1264.4" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-51)">/</text><text class="breeze-prod-image [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1288.8" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-52)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1288.8" textLength="475.8" clip-path="url(#breeze-prod-image-build-line-52)">ow-fallback-no-constraints-installation</text><text class="breeze-prod-image-build-r1" x="744.2" y="1288.8" textLength="244" clip-path="url(#breeze-prod-image-build-line-52)">installation fails. </text><text cla [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1313.2" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-53)">│</text><text class="breeze-prod-image-build-r5" x="744.2" y="1313.2" textLength="549" clip-path="url(#breeze-prod-image-build-line-53)">airflow-fallback-no-constraints-installation]</text><text class="breeze-prod-image-build-r5" x="1451.8" y="1313.2" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-53)">│</text><text class="breeze-prod-image- [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1337.6" textLength="1464" clip-path="url(#breeze-prod-image-build-line-54)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-prod-image-build-r1" x="1464" y="1337.6" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-54)">
+</text><text class="breeze-prod-image-build-r5" x="0" y="1362" textLength="24.4" clip-path="url(#breeze-prod-image-build-line-55)">â•─</text><text class="breeze-prod-image-build-r5" x="24.4" y="1362" textLength="634.4" clip-path="url(#breeze-prod-image-build-line-55)"> Choosing dependencies and extras (for power users) </text><text class="breeze-prod-image-build-r5" x="658.8" y="1362" textLength="780.8" clip-path="url(#breeze-prod-image-build-line-5 [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1386.4" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-56)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1386.4" textLength="390.4" clip-path="url(#breeze-prod-image-build-line-56)">--airflow-extras                </text><text class="breeze-prod-image-build-r1" x="439.2" y="1386.4" textLength="366" clip-path="url(#breeze- [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1410.8" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-57)">│</text><text class="breeze-prod-image-build-r5" x="439.2" y="1410.8" textLength="1000.4" clip-path="url(#breeze-prod-image-build-line-57)">aiobotocore,amazon,async,celery,cncf-kubernetes,common-io,common-messaging,docker,</text><text class="breeze-prod-image-build-r5" x="1451.8" y="1410.8" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-57)" [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1435.2" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-58)">│</text><text class="breeze-prod-image-build-r5" x="439.2" y="1435.2" textLength="1000.4" clip-path="url(#breeze-prod-image-build-line-58)">elasticsearch,fab,ftp,git,google,google-auth,graphviz,grpc,hashicorp,http,ldap,mic</text><text class="breeze-prod-image-build-r5" x="1451.8" y="1435.2" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-58)" [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1459.6" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-59)">│</text><text class="breeze-prod-image-build-r5" x="439.2" y="1459.6" textLength="1000.4" clip-path="url(#breeze-prod-image-build-line-59)">rosoft-azure,mysql,odbc,openlineage,opensearch,pandas,postgres,redis,sendgrid,sftp</text><text class="breeze-prod-image-build-r5" x="1451.8" y="1459.6" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-59)" [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1484" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-60)">│</text><text class="breeze-prod-image-build-r5" x="439.2" y="1484" textLength="378.2" clip-path="url(#breeze-prod-image-build-line-60)">,slack,snowflake,ssh,statsd,uv]</text><text class="breeze-prod-image-build-r6" x="829.6" y="1484" textLength="73.2" clip-path="url(#breeze-prod-image-build-line-60)">(TEXT)</text><text class="breeze-prod-image-build-r5" x="1 [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1508.4" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-61)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1508.4" textLength="390.4" clip-path="url(#breeze-prod-image-build-line-61)">--additional-airflow-extras     </text><text class="breeze-prod-image-build-r1" x="439.2" y="1508.4" textLength="780.8" clip-path="url(#breeze-prod-image-build-line-61)">Additional extra  [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1532.8" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-62)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1532.8" textLength="390.4" clip-path="url(#breeze-prod-image-build-line-62)">--additional-python-deps        </text><text class="breeze-prod-image-build-r1" x="439.2" y="1532.8" textLength="780.8" clip-path="url(#breeze-prod-image-build-line-62)">Additional& [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1557.2" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-63)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1557.2" textLength="390.4" clip-path="url(#breeze-prod-image-build-line-63)">--dev-apt-deps                  </text><text class="breeze-prod-image-build-r1" x="439.2" y="1557.2" textLength="658.8" clip-path=" [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1581.6" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-64)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1581.6" textLength="390.4" clip-path="url(#breeze-prod-image-build-line-64)">--additional-dev-apt-deps       </text><text class="breeze-prod-image-build-r1" x="439.2" y="1581.6" textLength="793" clip-path="url(#breeze-prod-image-build-line-64)">Additional ap [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1606" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-65)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1606" textLength="390.4" clip-path="url(#breeze-prod-image-build-line-65)">--dev-apt-command               </text><text class="breeze-prod-image-build-r1" x="439.2" y="1606" textLength="634.4" clip-path="url(#breeze-prod-imag [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1630.4" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-66)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1630.4" textLength="390.4" clip-path="url(#breeze-prod-image-build-line-66)">--additional-dev-apt-command    </text><text class="breeze-prod-image-build-r1" x="439.2" y="1630.4" textLength="768.6" clip-path="url(#breeze-prod-image-build-line-66)">Additional command ex [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1654.8" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-67)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1654.8" textLength="390.4" clip-path="url(#breeze-prod-image-build-line-67)">--additional-dev-apt-env        </text><text class="breeze-prod-image-build-r1" x="439.2" y="1654.8" textLength="817.4" clip-path="url(#breeze-prod-image-build-line-67)">Additional& [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1679.2" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-68)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1679.2" textLength="390.4" clip-path="url(#breeze-prod-image-build-line-68)">--runtime-apt-deps              </text><text class="breeze-prod-image-build-r1" x="439.2" y="1679.2" textLength="707.6" clip-path="url(#breeze-prod-ima [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1703.6" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-69)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1703.6" textLength="390.4" clip-path="url(#breeze-prod-image-build-line-69)">--additional-runtime-apt-deps   </text><text class="breeze-prod-image-build-r1" x="439.2" y="1703.6" textLength="841.8" clip-path="url(#breeze-prod-image-build-line-69)">Additional apt runtime [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1728" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-70)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1728" textLength="390.4" clip-path="url(#breeze-prod-image-build-line-70)">--runtime-apt-command           </text><text class="breeze-prod-image-build-r1" x="439.2" y="1728" textLength="683.2" clip-path="url(#breeze-prod-image-build-line-70)">Co [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1752.4" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-71)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1752.4" textLength="390.4" clip-path="url(#breeze-prod-image-build-line-71)">--additional-runtime-apt-command</text><text class="breeze-prod-image-build-r1" x="439.2" y="1752.4" textLength="817.4" clip-path="url(#breeze-prod-image-build-line-71)">Additional command executed before&# [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1776.8" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-72)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1776.8" textLength="390.4" clip-path="url(#breeze-prod-image-build-line-72)">--additional-runtime-apt-env    </text><text class="breeze-prod-image-build-r1" x="439.2" y="1776.8" textLength="866.2" clip-path="url(#breeze-prod-image-build-line-72)">Additional environment [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1801.2" textLength="1464" clip-path="url(#breeze-prod-image-build-line-73)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-prod-image-build-r1" x="1464" y="1801.2" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-73)">
+</text><text class="breeze-prod-image-build-r5" x="0" y="1825.6" textLength="24.4" clip-path="url(#breeze-prod-image-build-line-74)">â•─</text><text class="breeze-prod-image-build-r5" x="24.4" y="1825.6" textLength="817.4" clip-path="url(#breeze-prod-image-build-line-74)"> Advanced customization options (for specific customization needs) </text><text class="breeze-prod-image-build-r5" x="841.8" y="1825.6" textLength="597.8" clip-path="url(#breeze-pr [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1850" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-75)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1850" textLength="524.6" clip-path="url(#breeze-prod-image-build-line-75)">--installation-method                      </text><text class="breeze-prod-image-build-r1" x="573.4" y="1850" textL [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1874.4" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-76)">│</text><text class="breeze-prod-image-build-r5" x="573.4" y="1874.4" textLength="146.4" clip-path="url(#breeze-prod-image-build-line-76)">[default: .]</text><text class="breeze-prod-image-build-r6" x="732" y="1874.4" textLength="244" clip-path="url(#breeze-prod-image-build-line-76)">(. | apache-airflow)</text><text class="breeze-prod-image-b [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1898.8" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-77)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1898.8" textLength="524.6" clip-path="url(#breeze-prod-image-build-line-77)">--install-airflow-reference                </text><text class="breeze-prod-image-build-r1" x="573.4" y="1898.8" textLength="536.8" clip-path= [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1923.2" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-78)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1923.2" textLength="524.6" clip-path="url(#breeze-prod-image-build-line-78)">--install-distributions-from-context       </text><text class="breeze-prod-image-build-r1" x="573.4" y="1923.2" textLength="866.2" clip-path="url(#breeze-prod-image-build-line-78)">Insta [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1947.6" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-79)">│</text><text class="breeze-prod-image-build-r1" x="573.4" y="1947.6" textLength="183" clip-path="url(#breeze-prod-image-build-line-79)">image. Implies </text><text class="breeze-prod-image-build-r4" x="756.4" y="1947.6" textLength="341.6" clip-path="url(#breeze-prod-image-build-line-79)">--disable-airflow-repo-cache</text><text class="breeze-prod [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1972" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-80)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1972" textLength="524.6" clip-path="url(#breeze-prod-image-build-line-80)">--install-mysql-client-type                </text><text class="breeze-prod-image-build-r1" x="573.4" y="1972" textLength="488" clip-path="url(#br [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="1996.4" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-81)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="1996.4" textLength="524.6" clip-path="url(#breeze-prod-image-build-line-81)">--cleanup-context                          </text><text class="breeze-prod-image-build-r1" [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="2020.8" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-82)">│</text><text class="breeze-prod-image-build-r1" x="573.4" y="2020.8" textLength="170.8" clip-path="url(#breeze-prod-image-build-line-82)">together with </text><text class="breeze-prod-image-build-r4" x="744.2" y="2020.8" textLength="439.2" clip-path="url(#breeze-prod-image-build-line-82)">--install-distributions-from-context</text><text class="br [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="2045.2" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-83)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="2045.2" textLength="524.6" clip-path="url(#breeze-prod-image-build-line-83)">--use-constraints-for-context-distributions</text><text class="breeze-prod-image-build-r1" x="573.4" y="2045.2" textLength="866.2" clip-path="url(#breeze-prod-image-build-line-83)">Uses constraints for cont [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="2069.6" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-84)">│</text><text class="breeze-prod-image-build-r1" x="573.4" y="2069.6" textLength="866.2" clip-path="url(#breeze-prod-image-build-line-84)">constraints store in docker-context-files or from github.              </text><text class="breeze-prod-image-build-r5" x [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="2094" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-85)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="2094" textLength="524.6" clip-path="url(#breeze-prod-image-build-line-85)">--disable-airflow-repo-cache               </text><text class="breeze-prod-image-build-r1" x="573.4" y="2094" textLength="658.8" clip-path="url(#breez [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="2118.4" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-86)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="2118.4" textLength="524.6" clip-path="url(#breeze-prod-image-build-line-86)">--disable-mysql-client-installation        </text><text class="breeze-prod-image-build-r1" x="573.4" y="2118.4" textLength="341.6" clip-path="url(#breeze-prod-image-build-line-86)"> [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="2142.8" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-87)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="2142.8" textLength="524.6" clip-path="url(#breeze-prod-image-build-line-87)">--disable-mssql-client-installation        </text><text class="breeze-prod-image-build-r1" x="573.4" y="2142.8" textLength="341.6" clip-path="url(#breeze-prod-image-build-line-87)"> [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="2167.2" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-88)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="2167.2" textLength="524.6" clip-path="url(#breeze-prod-image-build-line-88)">--disable-postgres-client-installation     </text><text class="breeze-prod-image-build-r1" x="573.4" y="2167.2" textLength="378.2" clip-path="url(#breeze-prod-image-build-line-88)">Do not [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="2191.6" textLength="1464" clip-path="url(#breeze-prod-image-build-line-89)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-prod-image-build-r1" x="1464" y="2191.6" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-89)">
+</text><text class="breeze-prod-image-build-r5" x="0" y="2216" textLength="24.4" clip-path="url(#breeze-prod-image-build-line-90)">â•─</text><text class="breeze-prod-image-build-r5" x="24.4" y="2216" textLength="622.2" clip-path="url(#breeze-prod-image-build-line-90)"> Preparing cache and push (for maintainers and CI) </text><text class="breeze-prod-image-build-r5" x="646.6" y="2216" textLength="793" clip-path="url(#breeze-prod-image-build-line [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="2240.4" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-91)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="2240.4" textLength="268.4" clip-path="url(#breeze-prod-image-build-line-91)">--builder             </text><text class="breeze-prod-image-build-r1" x="317.2" y="2240.4" textLength="768.6" clip-path="url(#breeze-prod-image-build-line-9 [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="2264.8" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-92)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="2264.8" textLength="268.4" clip-path="url(#breeze-prod-image-build-line-92)">--platform            </text><text class="breeze-prod-image-build-r1" x="317.2" y="2264.8" textLength="341.6" clip-path="url(#breeze-prod-image-build-line-92)">P [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="2289.2" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-93)">│</text><text class="breeze-prod-image-build-r6" x="317.2" y="2289.2" textLength="292.8" clip-path="url(#breeze-prod-image-build-line-93)">linux/amd64,linux/arm64)</text><text class="breeze-prod-image-build-r5" x="1451.8" y="2289.2" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-93)">│</text><text class="breeze-prod-image-build-r1" x="1464" [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="2313.6" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-94)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="2313.6" textLength="268.4" clip-path="url(#breeze-prod-image-build-line-94)">--push                </text><text class="breeze-prod-image-build-r1" x="317.2" y="2313.6" textLength="353.8" clip-path="url(#breeze-prod-ima [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="2338" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-95)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="2338" textLength="268.4" clip-path="url(#breeze-prod-image-build-line-95)">--prepare-buildx-cache</text><text class="breeze-prod-image-build-r1" x="317.2" y="2338" textLength="1122.4" clip-path="url(#breeze-prod-image-build-line-95)">Prepares build cache (this is done a [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="2362.4" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-96)">│</text><text class="breeze-prod-image-build-r1" x="317.2" y="2362.4" textLength="1122.4" clip-path="url(#breeze-prod-image-build-line-96)">image).                                   &# [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="2386.8" textLength="1464" clip-path="url(#breeze-prod-image-build-line-97)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-prod-image-build-r1" x="1464" y="2386.8" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-97)">
+</text><text class="breeze-prod-image-build-r5" x="0" y="2411.2" textLength="24.4" clip-path="url(#breeze-prod-image-build-line-98)">â•─</text><text class="breeze-prod-image-build-r5" x="24.4" y="2411.2" textLength="280.6" clip-path="url(#breeze-prod-image-build-line-98)"> GitHub authentication </text><text class="breeze-prod-image-build-r5" x="305" y="2411.2" textLength="1134.6" clip-path="url(#breeze-prod-image-build-line-98)">───────────────────────────────────────────── [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="2435.6" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-99)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="2435.6" textLength="231.8" clip-path="url(#breeze-prod-image-build-line-99)">--github-repository</text><text class="breeze-prod-image-build-r7" x="280.6" y="2435.6" textLength="24.4" clip-path="url(#breeze-prod-image-build-line-99)">-g</text><text class="breeze-prod-image-build-r1" x="329.4" y="24 [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="2460" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-100)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="2460" textLength="231.8" clip-path="url(#breeze-prod-image-build-line-100)">--github-token     </text><text class="breeze-prod-image-build-r1" x="329.4" y="2460" textLength="512.4" clip-path="url(#breeze-prod-image-build-line-100)">The token used to auth [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="2484.4" textLength="1464" clip-path="url(#breeze-prod-image-build-line-101)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-prod-image-build-r1" x="1464" y="2484.4" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-101)">
+</text><text class="breeze-prod-image-build-r5" x="0" y="2508.8" textLength="24.4" clip-path="url(#breeze-prod-image-build-line-102)">â•─</text><text class="breeze-prod-image-build-r5" x="24.4" y="2508.8" textLength="195.2" clip-path="url(#breeze-prod-image-build-line-102)"> Common options </text><text class="breeze-prod-image-build-r5" x="219.6" y="2508.8" textLength="1220" clip-path="url(#breeze-prod-image-build-line-102)">───────────────────────────────────────────────── [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="2533.2" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-103)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="2533.2" textLength="109.8" clip-path="url(#breeze-prod-image-build-line-103)">--answer </text><text class="breeze-prod-image-build-r7" x="158.6" y="2533.2" textLength="24.4" clip-path="url(#breeze-prod-image-build-line-103)">-a</text><text class="breeze-prod-image-build-r1" x="207.4" y="2533 [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="2557.6" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-104)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="2557.6" textLength="109.8" clip-path="url(#breeze-prod-image-build-line-104)">--dry-run</text><text class="breeze-prod-image-build-r7" x="158.6" y="2557.6" textLength="24.4" clip-path="url(#breeze-prod-image-build-line-104)">-D</text><text class="breeze-prod-image-build-r1" x="207.4" y="2557.6" t [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="2582" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-105)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="2582" textLength="109.8" clip-path="url(#breeze-prod-image-build-line-105)">--verbose</text><text class="breeze-prod-image-build-r7" x="158.6" y="2582" textLength="24.4" clip-path="url(#breeze-prod-image-build-line-105)">-v</text><text class="breeze-prod-image-build-r1" x="207.4" y="2582" textLengt [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="2606.4" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-106)">│</text><text class="breeze-prod-image-build-r4" x="24.4" y="2606.4" textLength="109.8" clip-path="url(#breeze-prod-image-build-line-106)">--help   </text><text class="breeze-prod-image-build-r7" x="158.6" y="2606.4" textLength="24.4" clip-path="url(#breeze-prod-image-build-line-106)">-h</text><text class="breeze-prod-image-build-r1" x="207. [...]
+</text><text class="breeze-prod-image-build-r5" x="0" y="2630.8" textLength="1464" clip-path="url(#breeze-prod-image-build-line-107)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-prod-image-build-r1" x="1464" y="2630.8" textLength="12.2" clip-path="url(#breeze-prod-image-build-line-107)">
</text>
</g>
</g>
diff --git a/dev/breeze/doc/images/output_prod-image_build.txt b/dev/breeze/doc/images/output_prod-image_build.txt
index 4ed9dcbff5c..6d5ac39a2b3 100644
--- a/dev/breeze/doc/images/output_prod-image_build.txt
+++ b/dev/breeze/doc/images/output_prod-image_build.txt
@@ -1 +1 @@
-17aa276f049a23e6e313558318a774f9
+0919cf621f38bdfe2388bf3cc479d55d
diff --git a/dev/breeze/doc/images/output_release-management.svg b/dev/breeze/doc/images/output_release-management.svg
index ed3cab0d811..ab02bee7fd7 100644
--- a/dev/breeze/doc/images/output_release-management.svg
+++ b/dev/breeze/doc/images/output_release-management.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 1482 1709.1999999999998" xmlns="http://www.w3.org/2000/svg">
+<svg class="rich-terminal" viewBox="0 0 1482 1733.6" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@@ -42,7 +42,7 @@
<defs>
<clipPath id="breeze-release-management-clip-terminal">
- <rect x="0" y="0" width="1463.0" height="1658.1999999999998" />
+ <rect x="0" y="0" width="1463.0" height="1682.6" />
</clipPath>
<clipPath id="breeze-release-management-line-0">
<rect x="0" y="1.5" width="1464" height="24.65"/>
@@ -245,9 +245,12 @@
<clipPath id="breeze-release-management-line-66">
<rect x="0" y="1611.9" width="1464" height="24.65"/>
</clipPath>
+<clipPath id="breeze-release-management-line-67">
+ <rect x="0" y="1636.3" width="1464" height="24.65"/>
+ </clipPath>
</defs>
- <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1480" height="1707.2" rx="8"/><text class="breeze-release-management-title" fill="#c5c8c6" text-anchor="middle" x="740" y="27">Command: release-management</text>
+ <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1480" height="1731.6" rx="8"/><text class="breeze-release-management-title" fill="#c5c8c6" text-anchor="middle" x="740" y="27">Command: release-management</text>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
@@ -311,20 +314,21 @@
</text><text class="breeze-release-management-r5" x="0" y="1264.4" textLength="12.2" clip-path="url(#breeze-release-management-line-51)">│</text><text class="breeze-release-management-r4" x="24.4" y="1264.4" textLength="451.4" clip-path="url(#breeze-release-management-line-51)">prepare-mypy-distributions           </text><text class="breeze-release-management-r1" x="500.2" y="1264.4" textLength="939.4" clip-path="url(#breeze-release- [...]
</text><text class="breeze-release-management-r5" x="0" y="1288.8" textLength="1464" clip-path="url(#breeze-release-management-line-52)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-release-management-r1" x="1464" y="1288.8" textLength="12.2" clip-path="url(#breeze-release-management-line-52)">
</text><text class="breeze-release-management-r5" x="0" y="1313.2" textLength="24.4" clip-path="url(#breeze-release-management-line-53)">â•─</text><text class="breeze-release-management-r5" x="24.4" y="1313.2" textLength="292.8" clip-path="url(#breeze-release-management-line-53)"> Other release commands </text><text class="breeze-release-management-r5" x="317.2" y="1313.2" textLength="1122.4" clip-path="url(#breeze-release-management-line-53)">───────────────────────── [...]
-</text><text class="breeze-release-management-r5" x="0" y="1337.6" textLength="12.2" clip-path="url(#breeze-release-management-line-54)">│</text><text class="breeze-release-management-r4" x="24.4" y="1337.6" textLength="292.8" clip-path="url(#breeze-release-management-line-54)">add-back-references     </text><text class="breeze-release-management-r1" x="341.6" y="1337.6" textLength="1098" clip-path="url(#breeze-release-management-line-54)">Command to ad [...]
-</text><text class="breeze-release-management-r5" x="0" y="1362" textLength="12.2" clip-path="url(#breeze-release-management-line-55)">│</text><text class="breeze-release-management-r4" x="24.4" y="1362" textLength="292.8" clip-path="url(#breeze-release-management-line-55)">prepare-python-client   </text><text class="breeze-release-management-r1" x="341.6" y="1362" textLength="1098" clip-path="url(#breeze-release-management-line-55)">Prepares python client p [...]
-</text><text class="breeze-release-management-r5" x="0" y="1386.4" textLength="12.2" clip-path="url(#breeze-release-management-line-56)">│</text><text class="breeze-release-management-r4" x="24.4" y="1386.4" textLength="292.8" clip-path="url(#breeze-release-management-line-56)">publish-docs            </text><text class="breeze-release-management-r1" x="341.6" y="1386.4" textLength="1098" clip-path="url(#breeze-release-managemen [...]
-</text><text class="breeze-release-management-r5" x="0" y="1410.8" textLength="12.2" clip-path="url(#breeze-release-management-line-57)">│</text><text class="breeze-release-management-r4" x="24.4" y="1410.8" textLength="292.8" clip-path="url(#breeze-release-management-line-57)">generate-constraints    </text><text class="breeze-release-management-r1" x="341.6" y="1410.8" textLength="1098" clip-path="url(#breeze-release-management-line-57)">Generates pinned c [...]
-</text><text class="breeze-release-management-r5" x="0" y="1435.2" textLength="12.2" clip-path="url(#breeze-release-management-line-58)">│</text><text class="breeze-release-management-r4" x="24.4" y="1435.2" textLength="292.8" clip-path="url(#breeze-release-management-line-58)">update-constraints      </text><text class="breeze-release-management-r1" x="341.6" y="1435.2" textLength="1098" clip-path="url(#breeze-release-management-line-58)">Update releas [...]
-</text><text class="breeze-release-management-r5" x="0" y="1459.6" textLength="12.2" clip-path="url(#breeze-release-management-line-59)">│</text><text class="breeze-release-management-r4" x="24.4" y="1459.6" textLength="292.8" clip-path="url(#breeze-release-management-line-59)">publish-docs-to-s3      </text><text class="breeze-release-management-r1" x="341.6" y="1459.6" textLength="1098" clip-path="url(#breeze-release-management-line-59)">Publishes doc [...]
-</text><text class="breeze-release-management-r5" x="0" y="1484" textLength="12.2" clip-path="url(#breeze-release-management-line-60)">│</text><text class="breeze-release-management-r4" x="24.4" y="1484" textLength="292.8" clip-path="url(#breeze-release-management-line-60)">publish-schemas-to-s3   </text><text class="breeze-release-management-r1" x="341.6" y="1484" textLength="1098" clip-path="url(#breeze-release-management-line-60)">Publishes generated JSON  [...]
-</text><text class="breeze-release-management-r5" x="0" y="1508.4" textLength="12.2" clip-path="url(#breeze-release-management-line-61)">│</text><text class="breeze-release-management-r4" x="24.4" y="1508.4" textLength="292.8" clip-path="url(#breeze-release-management-line-61)">verify-rc-by-pmc        </text><text class="breeze-release-management-r1" x="341.6" y="1508.4" textLength="1098" clip-path="url(#breeze-release-management-line-61)">[EXPERIM [...]
-</text><text class="breeze-release-management-r5" x="0" y="1532.8" textLength="12.2" clip-path="url(#breeze-release-management-line-62)">│</text><text class="breeze-release-management-r4" x="24.4" y="1532.8" textLength="292.8" clip-path="url(#breeze-release-management-line-62)">check-release-files     </text><text class="breeze-release-management-r1" x="341.6" y="1532.8" textLength="1098" clip-path="url(#breeze-release-management-line-62)">Verify that a [...]
-</text><text class="breeze-release-management-r5" x="0" y="1557.2" textLength="1464" clip-path="url(#breeze-release-management-line-63)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-release-management-r1" x="1464" y="1557.2" textLength="12.2" clip-path="url(#breeze-release-management-line-63)">
-</text><text class="breeze-release-management-r5" x="0" y="1581.6" textLength="24.4" clip-path="url(#breeze-release-management-line-64)">â•─</text><text class="breeze-release-management-r5" x="24.4" y="1581.6" textLength="122" clip-path="url(#breeze-release-management-line-64)"> Commands </text><text class="breeze-release-management-r5" x="146.4" y="1581.6" textLength="1293.2" clip-path="url(#breeze-release-management-line-64)">─────────────────────────────────────────────────── [...]
-</text><text class="breeze-release-management-r5" x="0" y="1606" textLength="12.2" clip-path="url(#breeze-release-management-line-65)">│</text><text class="breeze-release-management-r4" x="24.4" y="1606" textLength="305" clip-path="url(#breeze-release-management-line-65)">constraints-version-check</text><text class="breeze-release-management-r1" x="353.8" y="1606" textLength="1085.8" clip-path="url(#breeze-release-management-line-65)">Check constraints against released [...]
-</text><text class="breeze-release-management-r5" x="0" y="1630.4" textLength="12.2" clip-path="url(#breeze-release-management-line-66)">│</text><text class="breeze-release-management-r4" x="24.4" y="1630.4" textLength="305" clip-path="url(#breeze-release-management-line-66)">merge-prod-images        </text><text class="breeze-release-management-r1" x="353.8" y="1630.4" textLength="1085.8" clip-path="url(#breeze-release-management-line-66)">Merge&# [...]
-</text><text class="breeze-release-management-r5" x="0" y="1654.8" textLength="1464" clip-path="url(#breeze-release-management-line-67)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-release-management-r1" x="1464" y="1654.8" textLength="12.2" clip-path="url(#breeze-release-management-line-67)">
+</text><text class="breeze-release-management-r5" x="0" y="1337.6" textLength="12.2" clip-path="url(#breeze-release-management-line-54)">│</text><text class="breeze-release-management-r4" x="24.4" y="1337.6" textLength="280.6" clip-path="url(#breeze-release-management-line-54)">add-back-references    </text><text class="breeze-release-management-r1" x="329.4" y="1337.6" textLength="1110.2" clip-path="url(#breeze-release-management-line-54)">Command to add [...]
+</text><text class="breeze-release-management-r5" x="0" y="1362" textLength="12.2" clip-path="url(#breeze-release-management-line-55)">│</text><text class="breeze-release-management-r4" x="24.4" y="1362" textLength="280.6" clip-path="url(#breeze-release-management-line-55)">prepare-python-client  </text><text class="breeze-release-management-r1" x="329.4" y="1362" textLength="1110.2" clip-path="url(#breeze-release-management-line-55)">Prepares python client packa [...]
+</text><text class="breeze-release-management-r5" x="0" y="1386.4" textLength="12.2" clip-path="url(#breeze-release-management-line-56)">│</text><text class="breeze-release-management-r4" x="24.4" y="1386.4" textLength="280.6" clip-path="url(#breeze-release-management-line-56)">publish-docs           </text><text class="breeze-release-management-r1" x="329.4" y="1386.4" textLength="1110.2" clip-path="url(#breeze-release-management-li [...]
+</text><text class="breeze-release-management-r5" x="0" y="1410.8" textLength="12.2" clip-path="url(#breeze-release-management-line-57)">│</text><text class="breeze-release-management-r4" x="24.4" y="1410.8" textLength="280.6" clip-path="url(#breeze-release-management-line-57)">generate-constraints   </text><text class="breeze-release-management-r1" x="329.4" y="1410.8" textLength="1110.2" clip-path="url(#breeze-release-management-line-57)">Generates pinned const [...]
+</text><text class="breeze-release-management-r5" x="0" y="1435.2" textLength="12.2" clip-path="url(#breeze-release-management-line-58)">│</text><text class="breeze-release-management-r4" x="24.4" y="1435.2" textLength="280.6" clip-path="url(#breeze-release-management-line-58)">update-constraints     </text><text class="breeze-release-management-r1" x="329.4" y="1435.2" textLength="1110.2" clip-path="url(#breeze-release-management-line-58)">Update released&# [...]
+</text><text class="breeze-release-management-r5" x="0" y="1459.6" textLength="12.2" clip-path="url(#breeze-release-management-line-59)">│</text><text class="breeze-release-management-r4" x="24.4" y="1459.6" textLength="280.6" clip-path="url(#breeze-release-management-line-59)">publish-docs-to-s3     </text><text class="breeze-release-management-r1" x="329.4" y="1459.6" textLength="1110.2" clip-path="url(#breeze-release-management-line-59)">Publishes docs [...]
+</text><text class="breeze-release-management-r5" x="0" y="1484" textLength="12.2" clip-path="url(#breeze-release-management-line-60)">│</text><text class="breeze-release-management-r4" x="24.4" y="1484" textLength="280.6" clip-path="url(#breeze-release-management-line-60)">publish-schemas-to-s3  </text><text class="breeze-release-management-r1" x="329.4" y="1484" textLength="1110.2" clip-path="url(#breeze-release-management-line-60)">Publishes generated JSON sch [...]
+</text><text class="breeze-release-management-r5" x="0" y="1508.4" textLength="12.2" clip-path="url(#breeze-release-management-line-61)">│</text><text class="breeze-release-management-r4" x="24.4" y="1508.4" textLength="280.6" clip-path="url(#breeze-release-management-line-61)">verify-rc-by-pmc       </text><text class="breeze-release-management-r1" x="329.4" y="1508.4" textLength="1110.2" clip-path="url(#breeze-release-management-line-61)">[EXPERIMENTA [...]
+</text><text class="breeze-release-management-r5" x="0" y="1532.8" textLength="12.2" clip-path="url(#breeze-release-management-line-62)">│</text><text class="breeze-release-management-r4" x="24.4" y="1532.8" textLength="280.6" clip-path="url(#breeze-release-management-line-62)">check-release-files    </text><text class="breeze-release-management-r1" x="329.4" y="1532.8" textLength="1110.2" clip-path="url(#breeze-release-management-line-62)">Verify that all&# [...]
+</text><text class="breeze-release-management-r5" x="0" y="1557.2" textLength="12.2" clip-path="url(#breeze-release-management-line-63)">│</text><text class="breeze-release-management-r4" x="24.4" y="1557.2" textLength="280.6" clip-path="url(#breeze-release-management-line-63)">mirror-base-images     </text><text class="breeze-release-management-r1" x="329.4" y="1557.2" textLength="1110.2" clip-path="url(#breeze-release-management-line-63)">Mirror the h [...]
+</text><text class="breeze-release-management-r5" x="0" y="1581.6" textLength="1464" clip-path="url(#breeze-release-management-line-64)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-release-management-r1" x="1464" y="1581.6" textLength="12.2" clip-path="url(#breeze-release-management-line-64)">
+</text><text class="breeze-release-management-r5" x="0" y="1606" textLength="24.4" clip-path="url(#breeze-release-management-line-65)">â•─</text><text class="breeze-release-management-r5" x="24.4" y="1606" textLength="122" clip-path="url(#breeze-release-management-line-65)"> Commands </text><text class="breeze-release-management-r5" x="146.4" y="1606" textLength="1293.2" clip-path="url(#breeze-release-management-line-65)">───────────────────────────────────────────────────────── [...]
+</text><text class="breeze-release-management-r5" x="0" y="1630.4" textLength="12.2" clip-path="url(#breeze-release-management-line-66)">│</text><text class="breeze-release-management-r4" x="24.4" y="1630.4" textLength="305" clip-path="url(#breeze-release-management-line-66)">constraints-version-check</text><text class="breeze-release-management-r1" x="353.8" y="1630.4" textLength="1085.8" clip-path="url(#breeze-release-management-line-66)">Check constraints against releas [...]
+</text><text class="breeze-release-management-r5" x="0" y="1654.8" textLength="12.2" clip-path="url(#breeze-release-management-line-67)">│</text><text class="breeze-release-management-r4" x="24.4" y="1654.8" textLength="305" clip-path="url(#breeze-release-management-line-67)">merge-prod-images        </text><text class="breeze-release-management-r1" x="353.8" y="1654.8" textLength="1085.8" clip-path="url(#breeze-release-management-line-67)">Merge&# [...]
+</text><text class="breeze-release-management-r5" x="0" y="1679.2" textLength="1464" clip-path="url(#breeze-release-management-line-68)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-release-management-r1" x="1464" y="1679.2" textLength="12.2" clip-path="url(#breeze-release-management-line-68)">
</text>
</g>
</g>
diff --git a/dev/breeze/doc/images/output_release-management.txt b/dev/breeze/doc/images/output_release-management.txt
index 1a2955706e0..98674687d9e 100644
--- a/dev/breeze/doc/images/output_release-management.txt
+++ b/dev/breeze/doc/images/output_release-management.txt
@@ -1 +1 @@
-186f43f8d9e9e522daa2e16d537a46d3
+0b7200fc4be8b419c11762aff2314daf
diff --git a/dev/breeze/doc/images/output_release-management_mirror-base-images.svg b/dev/breeze/doc/images/output_release-management_mirror-base-images.svg
new file mode 100644
index 00000000000..94fe748b0d4
--- /dev/null
+++ b/dev/breeze/doc/images/output_release-management_mirror-base-images.svg
@@ -0,0 +1,116 @@
+<svg class="rich-terminal" viewBox="0 0 1482 391.59999999999997" xmlns="http://www.w3.org/2000/svg">
+ <!-- Generated with Rich https://www.textualize.io -->
+ <style>
+
+ @font-face {
+ font-family: "Fira Code";
+ src: local("FiraCode-Regular"),
+ url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
+ url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
+ font-style: normal;
+ font-weight: 400;
+ }
+ @font-face {
+ font-family: "Fira Code";
+ src: local("FiraCode-Bold"),
+ url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
+ url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
+ font-style: bold;
+ font-weight: 700;
+ }
+
+ .breeze-release-management-mirror-base-images-matrix {
+ font-family: Fira Code, monospace;
+ font-size: 20px;
+ line-height: 24.4px;
+ font-variant-east-asian: full-width;
+ }
+
+ .breeze-release-management-mirror-base-images-title {
+ font-size: 18px;
+ font-weight: bold;
+ font-family: arial;
+ }
+
+ .breeze-release-management-mirror-base-images-r1 { fill: #c5c8c6 }
+.breeze-release-management-mirror-base-images-r2 { fill: #d0b344 }
+.breeze-release-management-mirror-base-images-r3 { fill: #c5c8c6;font-weight: bold }
+.breeze-release-management-mirror-base-images-r4 { fill: #68a0b3;font-weight: bold }
+.breeze-release-management-mirror-base-images-r5 { fill: #868887 }
+.breeze-release-management-mirror-base-images-r6 { fill: #98a84b;font-weight: bold }
+.breeze-release-management-mirror-base-images-r7 { fill: #8d7b39 }
+ </style>
+
+ <defs>
+ <clipPath id="breeze-release-management-mirror-base-images-clip-terminal">
+ <rect x="0" y="0" width="1463.0" height="340.59999999999997" />
+ </clipPath>
+ <clipPath id="breeze-release-management-mirror-base-images-line-0">
+ <rect x="0" y="1.5" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-release-management-mirror-base-images-line-1">
+ <rect x="0" y="25.9" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-release-management-mirror-base-images-line-2">
+ <rect x="0" y="50.3" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-release-management-mirror-base-images-line-3">
+ <rect x="0" y="74.7" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-release-management-mirror-base-images-line-4">
+ <rect x="0" y="99.1" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-release-management-mirror-base-images-line-5">
+ <rect x="0" y="123.5" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-release-management-mirror-base-images-line-6">
+ <rect x="0" y="147.9" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-release-management-mirror-base-images-line-7">
+ <rect x="0" y="172.3" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-release-management-mirror-base-images-line-8">
+ <rect x="0" y="196.7" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-release-management-mirror-base-images-line-9">
+ <rect x="0" y="221.1" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-release-management-mirror-base-images-line-10">
+ <rect x="0" y="245.5" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-release-management-mirror-base-images-line-11">
+ <rect x="0" y="269.9" width="1464" height="24.65"/>
+ </clipPath>
+<clipPath id="breeze-release-management-mirror-base-images-line-12">
+ <rect x="0" y="294.3" width="1464" height="24.65"/>
+ </clipPath>
+ </defs>
+
+ <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1480" height="389.6" rx="8"/><text class="breeze-release-management-mirror-base-images-title" fill="#c5c8c6" text-anchor="middle" x="740" y="27">Command: release-management mirror-base-images</text>
+ <g transform="translate(26,22)">
+ <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
+ <circle cx="22" cy="0" r="7" fill="#febc2e"/>
+ <circle cx="44" cy="0" r="7" fill="#28c840"/>
+ </g>
+
+ <g transform="translate(9, 41)" clip-path="url(#breeze-release-management-mirror-base-images-clip-terminal)">
+
+ <g class="breeze-release-management-mirror-base-images-matrix">
+ <text class="breeze-release-management-mirror-base-images-r1" x="1464" y="20" textLength="12.2" clip-path="url(#breeze-release-management-mirror-base-images-line-0)">
+</text><text class="breeze-release-management-mirror-base-images-r2" x="12.2" y="44.4" textLength="73.2" clip-path="url(#breeze-release-management-mirror-base-images-line-1)">Usage:</text><text class="breeze-release-management-mirror-base-images-r3" x="97.6" y="44.4" textLength="536.8" clip-path="url(#breeze-release-management-mirror-base-images-line-1)">breeze release-management mirror-base-images</text><text class="breeze-release-management-mirror-base-images-r1" x="646.6" y= [...]
+</text><text class="breeze-release-management-mirror-base-images-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#breeze-release-management-mirror-base-images-line-2)">
+</text><text class="breeze-release-management-mirror-base-images-r1" x="12.2" y="93.2" textLength="1049.2" clip-path="url(#breeze-release-management-mirror-base-images-line-3)">Mirror the hardened Python base images Airflow builds on to Airflow's public registry.</text><text class="breeze-release-management-mirror-base-images-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#breeze-release-management-mirror-base-image [...]
+</text><text class="breeze-release-management-mirror-base-images-r1" x="1464" y="117.6" textLength="12.2" clip-path="url(#breeze-release-management-mirror-base-images-line-4)">
+</text><text class="breeze-release-management-mirror-base-images-r5" x="0" y="142" textLength="24.4" clip-path="url(#breeze-release-management-mirror-base-images-line-5)">â•─</text><text class="breeze-release-management-mirror-base-images-r5" x="24.4" y="142" textLength="170.8" clip-path="url(#breeze-release-management-mirror-base-images-line-5)"> Mirror flags </text><text class="breeze-release-management-mirror-base-images-r5" x="195.2" y="142" textLength="1244.4" clip-pat [...]
+</text><text class="breeze-release-management-mirror-base-images-r5" x="0" y="166.4" textLength="12.2" clip-path="url(#breeze-release-management-mirror-base-images-line-6)">│</text><text class="breeze-release-management-mirror-base-images-r4" x="24.4" y="166.4" textLength="97.6" clip-path="url(#breeze-release-management-mirror-base-images-line-6)">--python</text><text class="breeze-release-management-mirror-base-images-r6" x="146.4" y="166.4" textLength="24.4" clip-path="url(#breeze-rele [...]
+</text><text class="breeze-release-management-mirror-base-images-r5" x="0" y="190.8" textLength="12.2" clip-path="url(#breeze-release-management-mirror-base-images-line-7)">│</text><text class="breeze-release-management-mirror-base-images-r1" x="195.2" y="190.8" textLength="134.2" clip-path="url(#breeze-release-management-mirror-base-images-line-7)">are used). </text><text class="breeze-release-management-mirror-base-images-r7" x="329.4" y="190.8" textLength="329.4" clip-path=" [...]
+</text><text class="breeze-release-management-mirror-base-images-r5" x="0" y="215.2" textLength="1464" clip-path="url(#breeze-release-management-mirror-base-images-line-8)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-release-management-mirror-base-images-r1" x="1464" y="215.2" textLength="12.2" clip-path="url(#breeze-release-management-mirror-base-images-line-8)">
+</text><text class="breeze-release-management-mirror-base-images-r5" x="0" y="239.6" textLength="24.4" clip-path="url(#breeze-release-management-mirror-base-images-line-9)">â•─</text><text class="breeze-release-management-mirror-base-images-r5" x="24.4" y="239.6" textLength="195.2" clip-path="url(#breeze-release-management-mirror-base-images-line-9)"> Common options </text><text class="breeze-release-management-mirror-base-images-r5" x="219.6" y="239.6" textLength="1220" cl [...]
+</text><text class="breeze-release-management-mirror-base-images-r5" x="0" y="264" textLength="12.2" clip-path="url(#breeze-release-management-mirror-base-images-line-10)">│</text><text class="breeze-release-management-mirror-base-images-r4" x="24.4" y="264" textLength="109.8" clip-path="url(#breeze-release-management-mirror-base-images-line-10)">--verbose</text><text class="breeze-release-management-mirror-base-images-r6" x="158.6" y="264" textLength="24.4" clip-path="url(#breeze-releas [...]
+</text><text class="breeze-release-management-mirror-base-images-r5" x="0" y="288.4" textLength="12.2" clip-path="url(#breeze-release-management-mirror-base-images-line-11)">│</text><text class="breeze-release-management-mirror-base-images-r4" x="24.4" y="288.4" textLength="109.8" clip-path="url(#breeze-release-management-mirror-base-images-line-11)">--dry-run</text><text class="breeze-release-management-mirror-base-images-r6" x="158.6" y="288.4" textLength="24.4" clip-path="url(#breeze- [...]
+</text><text class="breeze-release-management-mirror-base-images-r5" x="0" y="312.8" textLength="12.2" clip-path="url(#breeze-release-management-mirror-base-images-line-12)">│</text><text class="breeze-release-management-mirror-base-images-r4" x="24.4" y="312.8" textLength="109.8" clip-path="url(#breeze-release-management-mirror-base-images-line-12)">--help   </text><text class="breeze-release-management-mirror-base-images-r6" x="158.6" y="312.8" textLength="24.4" clip-pat [...]
+</text><text class="breeze-release-management-mirror-base-images-r5" x="0" y="337.2" textLength="1464" clip-path="url(#breeze-release-management-mirror-base-images-line-13)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-release-management-mirror-base-images-r1" x="1464" y="337.2" textLength="12.2" clip-path="url(#breeze-release-management-mirror-base-images-line-13)">
+</text>
+ </g>
+ </g>
+</svg>
diff --git a/dev/breeze/doc/images/output_release-management_mirror-base-images.txt b/dev/breeze/doc/images/output_release-management_mirror-base-images.txt
new file mode 100644
index 00000000000..3992f096330
--- /dev/null
+++ b/dev/breeze/doc/images/output_release-management_mirror-base-images.txt
@@ -0,0 +1 @@
+69322a6b07ada77a184bd781ff897a51
diff --git a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg
index ed6be314547..77c21159e89 100644
--- a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg
+++ b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg
@@ -231,16 +231,16 @@
</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="532.4" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-21)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="532.4" textLength="671" clip-path="url(#breeze-setup-check-all-params-in-groups-line-21)">release-management:generate-issue-content-helm-chart | </text><text class="breeze-setup-check-all-params-in-groups-r5" x="1451.8" y="532.4" textLength [...]
</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="556.8" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-22)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="556.8" textLength="1256.6" clip-path="url(#breeze-setup-check-all-params-in-groups-line-22)">release-management:generate-issue-content-providers | release-management:generate-providers-metadata | </text><text class="breeze-setup-c [...]
</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="581.2" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-23)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="581.2" textLength="1110.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-23)">release-management:install-provider-distributions | release-management:merge-prod-images | </text><text class="breeze-setup-check-all-par [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="605.6" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-24)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="605.6" textLength="1281" clip-path="url(#breeze-setup-check-all-params-in-groups-line-24)">release-management:prepare-airflow-ctl-distributions | release-management:prepare-airflow-distributions |</text><text class="breeze-setup-check- [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="630" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-25)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="630" textLength="1171.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-25)">release-management:prepare-helm-chart-package | release-management:prepare-helm-chart-tarball | </text><text class="breeze-setup-check-all-pa [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="654.4" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-26)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="654.4" textLength="1220" clip-path="url(#breeze-setup-check-all-params-in-groups-line-26)">release-management:prepare-mypy-distributions | release-management:prepare-provider-distributions | </text><text class="breeze-setup-check- [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="678.8" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-27)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="678.8" textLength="1159" clip-path="url(#breeze-setup-check-all-params-in-groups-line-27)">release-management:prepare-provider-documentation | release-management:prepare-python-client | </text><text class="breeze-setup-check-all-p [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="703.2" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-28)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="703.2" textLength="1085.8" clip-path="url(#breeze-setup-check-all-params-in-groups-line-28)">release-management:prepare-tarball | release-management:prepare-task-sdk-distributions | </text><text class="breeze-setup-check-all-param [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="727.6" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-29)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="727.6" textLength="902.8" clip-path="url(#breeze-setup-check-all-params-in-groups-line-29)">release-management:publish-docs | release-management:publish-docs-to-s3 | </text><text class="breeze-setup-check-all-params-in-groups-r5" [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="752" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-30)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="752" textLength="1024.8" clip-path="url(#breeze-setup-check-all-params-in-groups-line-30)">release-management:publish-schemas-to-s3 | release-management:release-prod-images | </text><text class="breeze-setup-check-all-params-in-grou [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="776.4" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-31)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="776.4" textLength="951.6" clip-path="url(#breeze-setup-check-all-params-in-groups-line-31)">release-management:start-beta-process | release-management:start-rc-process | </text><text class="breeze-setup-check-all-params-in-groups- [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="800.8" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-32)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="800.8" textLength="854" clip-path="url(#breeze-setup-check-all-params-in-groups-line-32)">release-management:start-release | release-management:tag-providers | </text><text class="breeze-setup-check-all-params-in-groups-r5" x="145 [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="825.2" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-33)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="825.2" textLength="1110.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-33)">release-management:update-constraints | release-management:update-providers-next-version | </text><text class="breeze-setup-check-all-par [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="605.6" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-24)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="605.6" textLength="1159" clip-path="url(#breeze-setup-check-all-params-in-groups-line-24)">release-management:mirror-base-images | release-management:prepare-airflow-ctl-distributions | </text><text class="breeze-setup-check-all-p [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="630" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-25)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="630" textLength="1207.8" clip-path="url(#breeze-setup-check-all-params-in-groups-line-25)">release-management:prepare-airflow-distributions | release-management:prepare-helm-chart-package | </text><text class="breeze-setup-check-all [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="654.4" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-26)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="654.4" textLength="1171.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-26)">release-management:prepare-helm-chart-tarball | release-management:prepare-mypy-distributions | </text><text class="breeze-setup-check-al [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="678.8" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-27)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="678.8" textLength="1268.8" clip-path="url(#breeze-setup-check-all-params-in-groups-line-27)">release-management:prepare-provider-distributions | release-management:prepare-provider-documentation | </text><text class="breeze-setup- [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="703.2" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-28)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="703.2" textLength="976" clip-path="url(#breeze-setup-check-all-params-in-groups-line-28)">release-management:prepare-python-client | release-management:prepare-tarball | </text><text class="breeze-setup-check-all-params-in-groups- [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="727.6" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-29)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="727.6" textLength="1049.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-29)">release-management:prepare-task-sdk-distributions | release-management:publish-docs | </text><text class="breeze-setup-check-all-params-i [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="752" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-30)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="752" textLength="1012.6" clip-path="url(#breeze-setup-check-all-params-in-groups-line-30)">release-management:publish-docs-to-s3 | release-management:publish-schemas-to-s3 | </text><text class="breeze-setup-check-all-params-in-group [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="776.4" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-31)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="776.4" textLength="988.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-31)">release-management:release-prod-images | release-management:start-beta-process | </text><text class="breeze-setup-check-all-params-in-grou [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="800.8" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-32)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="800.8" textLength="1281" clip-path="url(#breeze-setup-check-all-params-in-groups-line-32)">release-management:start-rc-process | release-management:start-release | release-management:tag-providers</text><text class="breeze-setup-c [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="825.2" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-33)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="825.2" textLength="1134.6" clip-path="url(#breeze-setup-check-all-params-in-groups-line-33)">| release-management:update-constraints | release-management:update-providers-next-version | </text><text class="breeze-setup-check- [...]
</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="849.6" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-34)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="849.6" textLength="1244.4" clip-path="url(#breeze-setup-check-all-params-in-groups-line-34)">release-management:verify-provider-distributions | release-management:verify-rc-by-pmc | run | sbom | </text><text cl [...]
</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="874" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-35)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="874" textLength="1281" clip-path="url(#breeze-setup-check-all-params-in-groups-line-35)">sbom:build-all-airflow-images | sbom:export-dependency-information | sbom:generate-providers-requirements</text><text class="breeze-setup-check [...]
</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="898.4" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-36)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="898.4" textLength="1183.4" clip-path="url(#breeze-setup-check-all-params-in-groups-line-36)">| sbom:update-sbom-information | setup | setup:autocomplete | setup:check-all-params-in-groups | </text><text cl [...]
diff --git a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.txt b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.txt
index 9d053954e72..872eb21c410 100644
--- a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.txt
+++ b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.txt
@@ -1 +1 @@
-cd743de2ac920faaec5a33b506ede093
+a706f8a030b5da92b951da442d1b85f2
diff --git a/dev/breeze/doc/images/output_setup_regenerate-command-images.svg b/dev/breeze/doc/images/output_setup_regenerate-command-images.svg
index a1ef0efe0d8..781ae597fd0 100644
--- a/dev/breeze/doc/images/output_setup_regenerate-command-images.svg
+++ b/dev/breeze/doc/images/output_setup_regenerate-command-images.svg
@@ -247,7 +247,7 @@
</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="556.8" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-22)">│</text><text class="breeze-setup-regenerate-command-images-r6" x="195.2" y="556.8" textLength="1244.4" clip-path="url(#breeze-setup-regenerate-command-images-line-22)">release-management:generate-issue-content-core | release-management:generate-issue-content-helm-chart </text><text class="breeze-setup-regenerate- [...]
</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="581.2" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-23)">│</text><text class="breeze-setup-regenerate-command-images-r6" x="195.2" y="581.2" textLength="1244.4" clip-path="url(#breeze-setup-regenerate-command-images-line-23)">| release-management:generate-issue-content-providers | release-management:generate-providers-metadata</text><text class="breeze-setup-regenerate- [...]
</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="605.6" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-24)">│</text><text class="breeze-setup-regenerate-command-images-r6" x="195.2" y="605.6" textLength="1134.6" clip-path="url(#breeze-setup-regenerate-command-images-line-24)">| release-management:install-provider-distributions | release-management:merge-prod-images | </text><text class="breeze-setup-regenerate [...]
-</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="630" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-25)">│</text><text class="breeze-setup-regenerate-command-images-r6" x="195.2" y="630" textLength="671" clip-path="url(#breeze-setup-regenerate-command-images-line-25)">release-management:prepare-airflow-ctl-distributions | </text><text class="breeze-setup-regenerate-command-images-r5" x="1451.8" y="630" textLength="12.2" cli [...]
+</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="630" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-25)">│</text><text class="breeze-setup-regenerate-command-images-r6" x="195.2" y="630" textLength="1159" clip-path="url(#breeze-setup-regenerate-command-images-line-25)">release-management:mirror-base-images | release-management:prepare-airflow-ctl-distributions | </text><text class="breeze-setup-regenerate-command- [...]
</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="654.4" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-26)">│</text><text class="breeze-setup-regenerate-command-images-r6" x="195.2" y="654.4" textLength="1207.8" clip-path="url(#breeze-setup-regenerate-command-images-line-26)">release-management:prepare-airflow-distributions | release-management:prepare-helm-chart-package | </text><text class="breeze-setup-regenerat [...]
</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="678.8" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-27)">│</text><text class="breeze-setup-regenerate-command-images-r6" x="195.2" y="678.8" textLength="1171.2" clip-path="url(#breeze-setup-regenerate-command-images-line-27)">release-management:prepare-helm-chart-tarball | release-management:prepare-mypy-distributions | </text><text class="breeze-setup-regenerate-c [...]
</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="703.2" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-28)">│</text><text class="breeze-setup-regenerate-command-images-r6" x="195.2" y="703.2" textLength="1244.4" clip-path="url(#breeze-setup-regenerate-command-images-line-28)">release-management:prepare-provider-distributions | release-management:prepare-provider-documentation </text><text class="breeze-setup-regenerate- [...]
diff --git a/dev/breeze/doc/images/output_setup_regenerate-command-images.txt b/dev/breeze/doc/images/output_setup_regenerate-command-images.txt
index c925b44aa64..3ad23387733 100644
--- a/dev/breeze/doc/images/output_setup_regenerate-command-images.txt
+++ b/dev/breeze/doc/images/output_setup_regenerate-command-images.txt
@@ -1 +1 @@
-b34bcd0ca3348d1279d48f96682a25e5
+2d92f8a4ea15a11eba4c63d0d0d7c912
diff --git a/dev/breeze/src/airflow_breeze/commands/ci_image_commands.py b/dev/breeze/src/airflow_breeze/commands/ci_image_commands.py
index c8ad18de046..ee3e0d342b0 100644
--- a/dev/breeze/src/airflow_breeze/commands/ci_image_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/ci_image_commands.py
@@ -38,7 +38,6 @@ from airflow_breeze.commands.common_image_options import (
option_airflow_constraints_reference_build,
option_build_progress,
option_cache_from_image,
- option_debian_version,
option_dev_apt_command,
option_dev_apt_deps,
option_disable_airflow_repo_cache,
@@ -252,7 +251,6 @@ option_ci_image_file_to_load = click.option(
@option_builder
@option_cache_from_image
@option_commit_sha
-@option_debian_version
@option_debug_resources
@option_dev_apt_command
@option_dev_apt_deps
@@ -292,7 +290,6 @@ def build(
builder: str,
cache_from_image: str | None,
commit_sha: str | None,
- debian_version: str,
debug_resources: bool,
dev_apt_command: str | None,
dev_apt_deps: str | None,
@@ -345,7 +342,6 @@ def build(
builder=builder,
cache_from_image=cache_from_image,
commit_sha=commit_sha,
- debian_version=debian_version,
dev_apt_command=dev_apt_command,
dev_apt_deps=dev_apt_deps,
disable_airflow_repo_cache=disable_airflow_repo_cache,
diff --git a/dev/breeze/src/airflow_breeze/commands/ci_image_commands_config.py b/dev/breeze/src/airflow_breeze/commands/ci_image_commands_config.py
index dd60ef4f2bf..d8fb175538d 100644
--- a/dev/breeze/src/airflow_breeze/commands/ci_image_commands_config.py
+++ b/dev/breeze/src/airflow_breeze/commands/ci_image_commands_config.py
@@ -59,7 +59,6 @@ CI_IMAGE_TOOLS_PARAMETERS: dict[str, list[dict[str, str | list[str]]]] = {
"--additional-pip-install-flags",
"--cache-from-image",
"--commit-sha",
- "--debian-version",
"--disable-airflow-repo-cache",
"--install-mysql-client-type",
"--python-image",
diff --git a/dev/breeze/src/airflow_breeze/commands/common_image_options.py b/dev/breeze/src/airflow_breeze/commands/common_image_options.py
index 696b1d93146..04a2c8d6a04 100644
--- a/dev/breeze/src/airflow_breeze/commands/common_image_options.py
+++ b/dev/breeze/src/airflow_breeze/commands/common_image_options.py
@@ -27,6 +27,7 @@ from airflow_breeze.global_constants import (
ALLOWED_BUILD_CACHE,
ALLOWED_BUILD_PROGRESS,
ALLOWED_DEBIAN_VERSIONS,
+ ALLOWED_IMAGE_FLAVORS,
ALLOWED_INSTALL_MYSQL_CLIENT_TYPES,
ALLOWED_PLATFORMS,
DOCKER_DEFAULT_PLATFORM,
@@ -115,6 +116,15 @@ option_debian_version = click.option(
help="Debian version used in Airflow image as base for building images.",
envvar="DEBIAN_VERSION",
)
+option_image_flavor = click.option(
+ "--image-flavor",
+ type=BetterChoice(ALLOWED_IMAGE_FLAVORS),
+ default=ALLOWED_IMAGE_FLAVORS[0],
+ show_default=True,
+ help="Flavor of the PROD image. 'hardened' gets Python from a Docker Hardened Image, "
+ "'legacy' compiles Python from sources on a plain Debian base image.",
+ envvar="IMAGE_FLAVOR",
+)
option_dev_apt_command = click.option(
"--dev-apt-command",
help="Command executed before dev apt deps are installed.",
diff --git a/dev/breeze/src/airflow_breeze/commands/kubernetes_commands.py b/dev/breeze/src/airflow_breeze/commands/kubernetes_commands.py
index 1b5761f9174..58bfab51fb5 100644
--- a/dev/breeze/src/airflow_breeze/commands/kubernetes_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/kubernetes_commands.py
@@ -20,6 +20,7 @@ import multiprocessing as mp
import os
import re
import shutil
+import subprocess
import sys
import tempfile
from collections.abc import Callable
@@ -2206,7 +2207,16 @@ def _run_complete_tests(
f"\n[info]Provisioning lang-SDK test env for Python {python}, "
f"Kubernetes {kubernetes_version}\n"
)
- _setup_lang_sdk_test(python=python, kubernetes_version=kubernetes_version, output=output)
+ try:
+ _setup_lang_sdk_test(python=python, kubernetes_version=kubernetes_version, output=output)
+ except subprocess.CalledProcessError as e:
+ # The cluster is deleted when this returns, so the pod logs have to be exported now
+ # for the failing release to be diagnosable at all.
+ _logs(python=python, kubernetes_version=kubernetes_version)
+ return (
+ e.returncode,
+ f"Lang-SDK test setup failed for Python {python}, Kubernetes {kubernetes_version}",
+ )
get_console(output=output).print(
f"\n[info]Running tests Python {python}, Kubernetes {kubernetes_version}\n"
)
diff --git a/dev/breeze/src/airflow_breeze/commands/production_image_commands.py b/dev/breeze/src/airflow_breeze/commands/production_image_commands.py
index 5d4add92f14..40db4093a73 100644
--- a/dev/breeze/src/airflow_breeze/commands/production_image_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/production_image_commands.py
@@ -44,6 +44,7 @@ from airflow_breeze.commands.common_image_options import (
option_from_run,
option_github_token_for_images,
option_image_file_dir,
+ option_image_flavor,
option_install_mysql_client_type,
option_platform_multiple,
option_prepare_buildx_cache,
@@ -260,6 +261,7 @@ def prod_image_group():
@option_dry_run
@option_github_repository
@option_github_token
+@option_image_flavor
@option_include_success_outputs
@option_install_mysql_client_type
@option_parallelism
@@ -313,6 +315,7 @@ def build(
docker_host: str | None,
github_repository: str,
github_token: str | None,
+ image_flavor: str,
include_success_outputs,
install_airflow_reference: str | None,
install_airflow_version: str | None,
@@ -411,6 +414,7 @@ def build(
cleanup_context=cleanup_context,
commit_sha=commit_sha,
debian_version=debian_version,
+ image_flavor=image_flavor,
dev_apt_command=dev_apt_command,
dev_apt_deps=dev_apt_deps,
docker_host=docker_host,
diff --git a/dev/breeze/src/airflow_breeze/commands/production_image_commands_config.py b/dev/breeze/src/airflow_breeze/commands/production_image_commands_config.py
index 46cf70f7ac9..feab7b40b09 100644
--- a/dev/breeze/src/airflow_breeze/commands/production_image_commands_config.py
+++ b/dev/breeze/src/airflow_breeze/commands/production_image_commands_config.py
@@ -56,6 +56,7 @@ PRODUCTION_IMAGE_TOOLS_PARAMETERS: dict[str, list[dict[str, str | list[str]]]] =
"--additional-pip-install-flags",
"--commit-sha",
"--debian-version",
+ "--image-flavor",
"--python-image",
"--skip-asset-compiled-check",
"--use-uv",
diff --git a/dev/breeze/src/airflow_breeze/commands/release_management_commands.py b/dev/breeze/src/airflow_breeze/commands/release_management_commands.py
index 472555b1685..39473330fb7 100644
--- a/dev/breeze/src/airflow_breeze/commands/release_management_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/release_management_commands.py
@@ -18,6 +18,7 @@ from __future__ import annotations
import ast
import glob
+import itertools
import operator
import os
import random
@@ -96,15 +97,20 @@ from airflow_breeze.global_constants import (
APACHE_AIRFLOW_GITHUB_REPOSITORY,
CONSTRAINTS,
CURRENT_PYTHON_MAJOR_MINOR_VERSIONS,
+ DEBIAN_VERSION_TO_HARDENED_IMAGE_DISTRO,
DEFAULT_PYTHON_MAJOR_MINOR_VERSION,
DEFAULT_PYTHON_MAJOR_MINOR_VERSION_FOR_IMAGES,
DESTINATION_LOCATIONS,
+ HARDENED_PYTHON_IMAGE_MIRROR,
+ HARDENED_PYTHON_IMAGE_SOURCE,
MULTI_PLATFORM,
SCHEMA_DESTINATION_LOCATIONS,
UV_VERSION,
get_airflow_mypy_version,
get_airflow_version,
get_airflowctl_version,
+ get_hardened_python_base_image,
+ get_hardened_python_image_tag,
get_task_sdk_version,
get_ts_sdk_version,
)
@@ -2474,7 +2480,7 @@ def release_prod_images(
for python in python_versions:
build_args = {
"AIRFLOW_CONSTRAINTS": "constraints-no-providers",
- "BASE_IMAGE": "debian:bookworm-slim",
+ "BASE_IMAGE": get_hardened_python_base_image(python),
"AIRFLOW_PYTHON_VERSION": ALL_PYTHON_VERSION_TO_PATCHLEVEL_VERSION.get(python, python),
"AIRFLOW_VERSION": airflow_version,
"INCLUDE_PRE_RELEASE": "true" if include_pre_release else "false",
@@ -5178,3 +5184,87 @@ def check_release_files(
else:
console.print("\n[success]All expected files are present![/]")
sys.exit(0)
+
+
+HARDENED_PYTHON_CATALOG_URL = (
+ "https://raw.githubusercontent.com/docker-hardened-images/catalog/main/image/python/{catalog_distro}/"
+ "{python}-dev.yaml"
+)
+
+
+def get_latest_hardened_python_patchlevel(
+ python: str, debian_version: str = ALLOWED_DEBIAN_VERSIONS[0]
+) -> str | None:
+ """
+ Return the newest patchlevel Docker publishes for a Python major.minor, or None if unknown.
+
+ Read from the image definition in Docker's public catalog. Mirroring it ahead of the pinned
+ patchlevel is what lets the "upgrade important versions" check bump the pin without the bump
+ pointing at a tag the mirror does not have yet.
+ """
+ import requests
+
+ distro = DEBIAN_VERSION_TO_HARDENED_IMAGE_DISTRO[debian_version]
+ # The catalog directory is "debian-13" for the images tagged "debian13".
+ catalog_distro = distro.replace("debian", "debian-", 1)
+ try:
+ response = requests.get(
+ HARDENED_PYTHON_CATALOG_URL.format(catalog_distro=catalog_distro, python=python), timeout=30
+ )
+ response.raise_for_status()
+ except requests.RequestException as e:
+ console_print(f"[warning]Could not read the hardened Python {python} {distro} definition: {e}[/]")
+ return None
+ versions = re.findall(rf"^\s*-\s*(\d+\.\d+\.\d+)-{distro}-dev\s*$", response.text, re.MULTILINE)
+ if not versions:
+ return None
+ return max(versions, key=lambda version: tuple(int(part) for part in version.split(".")))
+
+
+@release_management_group.command(
+ name="mirror-base-images",
+ help="Mirror the hardened Python base images Airflow builds on to Airflow's public registry.",
+)
+@option_python_no_default
+@option_verbose
+@option_dry_run
+def mirror_base_images(python: str | None):
+ python_versions = CURRENT_PYTHON_MAJOR_MINOR_VERSIONS if python is None else [python]
+ failed: list[str] = []
+ # Every Debian version is mirrored: the CI image and the default PROD image use the first one, the
+ # others are still available to build PROD images on.
+ for python_version, debian_version in itertools.product(python_versions, ALLOWED_DEBIAN_VERSIONS):
+ tag = get_hardened_python_image_tag(python_version, debian_version)
+ source = f"{HARDENED_PYTHON_IMAGE_SOURCE}:{tag}"
+ # The floating major/minor tag is what documentation and ad-hoc builds refer to, so that they
+ # do not have to be edited on every Python patch release.
+ floating_tag = tag.replace(
+ ALL_PYTHON_VERSION_TO_PATCHLEVEL_VERSION.get(python_version, python_version), python_version, 1
+ )
+ targets = [f"{HARDENED_PYTHON_IMAGE_MIRROR}:{tag}", f"{HARDENED_PYTHON_IMAGE_MIRROR}:{floating_tag}"]
+ copies = [(source, targets)]
+ pinned = ALL_PYTHON_VERSION_TO_PATCHLEVEL_VERSION.get(python_version, python_version)
+ latest = get_latest_hardened_python_patchlevel(python_version, debian_version)
+ if latest and latest != pinned:
+ latest_tag = tag.replace(pinned, latest, 1)
+ copies.append(
+ (
+ f"{HARDENED_PYTHON_IMAGE_SOURCE}:{latest_tag}",
+ [f"{HARDENED_PYTHON_IMAGE_MIRROR}:{latest_tag}"],
+ )
+ )
+ for copy_source, copy_targets in copies:
+ console_print(f"[info]Mirroring {copy_source} -> {', '.join(copy_targets)}[/]")
+ # imagetools copies the multi-platform manifest registry-to-registry, so the layers never
+ # travel through the machine running this.
+ tag_flags = [flag for target in copy_targets for flag in ("--tag", target)]
+ result = run_command(
+ ["docker", "buildx", "imagetools", "create", *tag_flags, copy_source],
+ check=False,
+ )
+ if result.returncode != 0:
+ failed.append(copy_source)
+ if failed:
+ console_print(f"[error]Failed to mirror: {', '.join(failed)}[/]")
+ sys.exit(1)
+ console_print("[success]All base images mirrored[/]")
diff --git a/dev/breeze/src/airflow_breeze/commands/release_management_commands_config.py b/dev/breeze/src/airflow_breeze/commands/release_management_commands_config.py
index 3cf90f73dc3..42a9761c2af 100644
--- a/dev/breeze/src/airflow_breeze/commands/release_management_commands_config.py
+++ b/dev/breeze/src/airflow_breeze/commands/release_management_commands_config.py
@@ -90,6 +90,7 @@ RELEASE_OTHER_COMMANDS: dict[str, str | list[str]] = {
"publish-schemas-to-s3",
"verify-rc-by-pmc",
"check-release-files",
+ "mirror-base-images",
],
}
@@ -104,6 +105,14 @@ RELEASE_MANAGEMENT_PARAMETERS: dict[str, list[dict[str, str | list[str]]]] = {
],
}
],
+ "breeze release-management mirror-base-images": [
+ {
+ "name": "Mirror flags",
+ "options": [
+ "--python",
+ ],
+ }
+ ],
"breeze release-management prepare-tarball": [
{
"name": "Tarball flags",
diff --git a/dev/breeze/src/airflow_breeze/global_constants.py b/dev/breeze/src/airflow_breeze/global_constants.py
index 18850b8a0f7..b57b90419a5 100644
--- a/dev/breeze/src/airflow_breeze/global_constants.py
+++ b/dev/breeze/src/airflow_breeze/global_constants.py
@@ -135,7 +135,12 @@ OPENLINEAGE_INTEGRATION = "openlineage"
OPENSEARCH_INTEGRATION = "opensearch"
OTHER_CORE_INTEGRATIONS = [STATSD_INTEGRATION, KEYCLOAK_INTEGRATION]
OTHER_PROVIDERS_INTEGRATIONS = [OPENLINEAGE_INTEGRATION]
-ALLOWED_DEBIAN_VERSIONS = ["bookworm"]
+# The first one is the default. Only the PROD image can be built on the others - the CI image is
+# always built on the default one.
+ALLOWED_DEBIAN_VERSIONS = ["trixie", "bookworm"]
+# "hardened" images get Python from a Docker Hardened Image base, "legacy" ones compile it from
+# source on a plain debian-slim base - the way all images were built before.
+ALLOWED_IMAGE_FLAVORS = ["hardened", "legacy"]
ALL_CORE_INTEGRATIONS = sorted(
[
*TESTABLE_CORE_INTEGRATIONS,
@@ -903,13 +908,36 @@ PROVIDERS_COMPATIBILITY_TESTS_MATRIX: list[dict[str, str | list[str]]] = [
]
ALL_PYTHON_VERSION_TO_PATCHLEVEL_VERSION: dict[str, str] = {
- "3.10": "3.10.21",
+ "3.10": "3.10.22",
"3.11": "3.11.17",
"3.12": "3.12.15",
"3.13": "3.13.16",
"3.14": "3.14.8",
}
+# Airflow images are based on Docker Hardened Images (https://dhi.io). The "-dev" variant carries
+# apt, a shell and runs as root; the tags encode the Debian release rather than its codename.
+# Pulling from dhi.io requires a Docker Hub login, so the tags Airflow builds against are mirrored
+# to a public ghcr.io repository by the "Mirror hardened base images" workflow - building Airflow
+# images needs no registry credentials at all, which is what the default below points at.
+HARDENED_PYTHON_IMAGE_SOURCE = "dhi.io/python"
+HARDENED_PYTHON_IMAGE_MIRROR = "ghcr.io/apache/airflow/base/python"
+DEBIAN_VERSION_TO_HARDENED_IMAGE_DISTRO = {"trixie": "debian13", "bookworm": "debian12"}
+
+
+def get_hardened_python_image_tag(python: str, debian_version: str = ALLOWED_DEBIAN_VERSIONS[0]) -> str:
+ distro = DEBIAN_VERSION_TO_HARDENED_IMAGE_DISTRO[debian_version]
+ return f"{ALL_PYTHON_VERSION_TO_PATCHLEVEL_VERSION.get(python, python)}-{distro}-dev"
+
+
+def get_hardened_python_base_image(python: str, debian_version: str = ALLOWED_DEBIAN_VERSIONS[0]) -> str:
+ return f"{HARDENED_PYTHON_IMAGE_MIRROR}:{get_hardened_python_image_tag(python, debian_version)}"
+
+
+def get_legacy_base_image(debian_version: str = ALLOWED_DEBIAN_VERSIONS[0]) -> str:
+ return f"debian:{debian_version}-slim"
+
+
# Number of slices for low dep tests
NUMBER_OF_LOW_DEP_SLICES = 5
diff --git a/dev/breeze/src/airflow_breeze/params/build_prod_params.py b/dev/breeze/src/airflow_breeze/params/build_prod_params.py
index fa32ca09644..d36543c9702 100644
--- a/dev/breeze/src/airflow_breeze/params/build_prod_params.py
+++ b/dev/breeze/src/airflow_breeze/params/build_prod_params.py
@@ -26,7 +26,9 @@ from airflow_breeze.global_constants import (
AIRFLOW_SOURCES_FROM,
AIRFLOW_SOURCES_TO,
ALL_PYTHON_VERSION_TO_PATCHLEVEL_VERSION,
+ ALLOWED_IMAGE_FLAVORS,
get_airflow_extras,
+ get_legacy_base_image,
)
from airflow_breeze.params.common_build_params import CommonBuildParams
from airflow_breeze.utils.console import console_print
@@ -45,6 +47,7 @@ class BuildProdParams(CommonBuildParams):
airflow_constraints_reference: str = DEFAULT_AIRFLOW_CONSTRAINTS_BRANCH
airflow_fallback_no_constraints_installation: bool = False
cleanup_context: bool = False
+ image_flavor: str = ALLOWED_IMAGE_FLAVORS[0]
airflow_extras: str = field(default_factory=get_airflow_extras)
disable_mssql_client_installation: bool = False
disable_mysql_client_installation: bool = False
@@ -58,6 +61,13 @@ class BuildProdParams(CommonBuildParams):
use_constraints_for_context_distributions: bool = False
use_uv: bool = True
+ @property
+ def python_base_image(self):
+ """The legacy flavor compiles Python itself, so it starts from plain Debian."""
+ if self.python_image is None and self.image_flavor == "legacy":
+ return get_legacy_base_image(self.debian_version)
+ return super().python_base_image
+
@property
def airflow_version(self) -> str:
if self.install_airflow_version:
@@ -232,6 +242,7 @@ class BuildProdParams(CommonBuildParams):
self._req_arg("DOCKER_CONTEXT_FILES", self.docker_context_files)
self._req_arg("INSTALL_DISTRIBUTIONS_FROM_CONTEXT", self.install_distributions_from_context)
self._req_arg("INSTALL_POSTGRES_CLIENT", self.install_postgres_client)
+ self._req_arg("AIRFLOW_IMAGE_FLAVOR", self.image_flavor)
self._req_arg("BASE_IMAGE", self.python_base_image)
self._req_arg(
"AIRFLOW_PYTHON_VERSION", ALL_PYTHON_VERSION_TO_PATCHLEVEL_VERSION.get(self.python, self.python)
diff --git a/dev/breeze/src/airflow_breeze/params/common_build_params.py b/dev/breeze/src/airflow_breeze/params/common_build_params.py
index 6ab34d7e80f..0dcccef3b84 100644
--- a/dev/breeze/src/airflow_breeze/params/common_build_params.py
+++ b/dev/breeze/src/airflow_breeze/params/common_build_params.py
@@ -25,10 +25,12 @@ from typing import Any
from airflow_breeze.branch_defaults import AIRFLOW_BRANCH, DEFAULT_AIRFLOW_CONSTRAINTS_BRANCH
from airflow_breeze.global_constants import (
ALLOWED_BUILD_PROGRESS,
+ ALLOWED_DEBIAN_VERSIONS,
ALLOWED_INSTALL_MYSQL_CLIENT_TYPES,
APACHE_AIRFLOW_GITHUB_REPOSITORY,
DOCKER_DEFAULT_PLATFORM,
get_airflow_version,
+ get_hardened_python_base_image,
)
from airflow_breeze.utils.console import console_print
from airflow_breeze.utils.platforms import get_normalized_platform
@@ -71,7 +73,7 @@ class CommonBuildParams:
dry_run: bool = False
version_suffix: str | None = None
verbose: bool = False
- debian_version: str = "bookworm"
+ debian_version: str = ALLOWED_DEBIAN_VERSIONS[0]
build_arg_values: list[str] = field(default_factory=list)
@property
@@ -124,7 +126,7 @@ class CommonBuildParams:
"""Construct Python Base Image"""
if self.python_image is not None:
return self.python_image
- return f"debian:{self.debian_version}-slim"
+ return get_hardened_python_base_image(self.python, self.debian_version)
@property
def airflow_image_repository(self):
diff --git a/dev/breeze/tests/test_build_prod_params.py b/dev/breeze/tests/test_build_prod_params.py
new file mode 100644
index 00000000000..659733cada1
--- /dev/null
+++ b/dev/breeze/tests/test_build_prod_params.py
@@ -0,0 +1,82 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import pytest
+
+from airflow_breeze.global_constants import ALL_PYTHON_VERSION_TO_PATCHLEVEL_VERSION
+from airflow_breeze.params.build_ci_params import BuildCiParams
+from airflow_breeze.params.build_prod_params import BuildProdParams
+
+
+def _build_args(params) -> dict[str, str]:
+ values = params.prepare_arguments_for_docker_build_command()
+ return dict(value.split("=", 1) for value in values[1::2])
+
+
+@pytest.mark.parametrize(
+ ("image_flavor", "debian_version", "expected_base_image"),
+ [
+ pytest.param(
+ "hardened",
+ "trixie",
+ f"ghcr.io/apache/airflow/base/python:{ALL_PYTHON_VERSION_TO_PATCHLEVEL_VERSION['3.12']}-debian13-dev",
+ id="hardened-trixie",
+ ),
+ pytest.param(
+ "hardened",
+ "bookworm",
+ f"ghcr.io/apache/airflow/base/python:{ALL_PYTHON_VERSION_TO_PATCHLEVEL_VERSION['3.12']}-debian12-dev",
+ id="hardened-bookworm",
+ ),
+ pytest.param("legacy", "trixie", "debian:trixie-slim", id="legacy-trixie"),
+ pytest.param("legacy", "bookworm", "debian:bookworm-slim", id="legacy-bookworm"),
+ ],
+)
+def test_prod_image_base_image_follows_flavor_and_debian_version(
+ image_flavor, debian_version, expected_base_image
+):
+ params = BuildProdParams(python="3.12", image_flavor=image_flavor, debian_version=debian_version)
+
+ build_args = _build_args(params)
+
+ assert build_args["BASE_IMAGE"] == expected_base_image
+ assert build_args["AIRFLOW_IMAGE_FLAVOR"] == image_flavor
+ assert build_args["AIRFLOW_PYTHON_VERSION"] == ALL_PYTHON_VERSION_TO_PATCHLEVEL_VERSION["3.12"]
+
+
+@pytest.mark.parametrize("image_flavor", ["hardened", "legacy"])
+def test_explicit_python_image_wins_over_flavor(image_flavor):
+ params = BuildProdParams(python="3.12", image_flavor=image_flavor, python_image="my-registry/python:x")
+
+ assert _build_args(params)["BASE_IMAGE"] == "my-registry/python:x"
+
+
+def test_prod_image_defaults_to_hardened_trixie():
+ build_args = _build_args(BuildProdParams(python="3.12"))
+
+ assert build_args["AIRFLOW_IMAGE_FLAVOR"] == "hardened"
+ assert build_args["BASE_IMAGE"].endswith("-debian13-dev")
+
+
+def test_ci_image_is_hardened_trixie():
+ build_args = _build_args(BuildCiParams(python="3.12"))
+
+ assert build_args["BASE_IMAGE"] == (
+ f"ghcr.io/apache/airflow/base/python:{ALL_PYTHON_VERSION_TO_PATCHLEVEL_VERSION['3.12']}-debian13-dev"
+ )
+ assert "AIRFLOW_IMAGE_FLAVOR" not in build_args
diff --git a/dev/breeze/tests/test_kubernetes_lang_sdk_commands.py b/dev/breeze/tests/test_kubernetes_lang_sdk_commands.py
index aa26675b6ac..56965024d67 100644
--- a/dev/breeze/tests/test_kubernetes_lang_sdk_commands.py
+++ b/dev/breeze/tests/test_kubernetes_lang_sdk_commands.py
@@ -591,3 +591,48 @@ class TestLangSdkResolveSdkSources:
mock_fetch.assert_called_once_with(tmp_path, None)
assert (go_sdk, java_sdk) == (tmp_path / "go-sdk", tmp_path / "java-sdk")
+
+
+class TestRunCompleteTestsLangSdkSetupFailure:
+ @mock.patch.object(kubernetes_commands, "_run_tests", autospec=True)
+ @mock.patch.object(kubernetes_commands, "_logs", autospec=True)
+ @mock.patch.object(
+ kubernetes_commands,
+ "_setup_lang_sdk_test",
+ autospec=True,
+ side_effect=kubernetes_commands.subprocess.CalledProcessError(1, ["helm", "upgrade"]),
+ )
+ @mock.patch.object(kubernetes_commands, "_deploy_airflow", autospec=True, return_value=(0, ""))
+ @mock.patch.object(
+ kubernetes_commands, "_preload_test_images_to_kind", autospec=True, return_value=(0, "")
+ )
+ @mock.patch.object(kubernetes_commands, "_upload_k8s_image", autospec=True, return_value=(0, ""))
+ @mock.patch.object(kubernetes_commands, "_configure_k8s_cluster", autospec=True, return_value=(0, ""))
+ @mock.patch.object(kubernetes_commands, "_create_cluster", autospec=True, return_value=(0, ""))
+ @mock.patch.object(kubernetes_commands, "_rebuild_k8s_image", autospec=True, return_value=(0, ""))
+ @mock.patch.object(kubernetes_commands, "_delete_cluster", autospec=True)
+ def test_failed_lang_sdk_setup_exports_logs_before_the_cluster_is_deleted(self, delete_cluster, *mocks):
+ run_tests, logs = mocks[-1], mocks[-2]
+ returncode, message = kubernetes_commands._run_complete_tests(
+ python="3.11",
+ kubernetes_version="v1.31.12",
+ include_success_outputs=False,
+ executor=kubernetes_commands.KUBERNETES_EXECUTOR,
+ rebuild_base_image=False,
+ copy_local_sources=False,
+ use_uv=True,
+ upgrade=False,
+ wait_time_in_seconds=0,
+ force_recreate_cluster=False,
+ use_standard_naming=False,
+ lang_sdk_test=True,
+ num_tries=1,
+ extra_options=None,
+ test_args=[],
+ output=None,
+ )
+
+ assert returncode == 1
+ assert "Lang-SDK test setup failed" in message
+ logs.assert_called_once_with(python="3.11", kubernetes_version="v1.31.12")
+ run_tests.assert_not_called()
diff --git a/dev/breeze/tests/test_mirror_base_images.py b/dev/breeze/tests/test_mirror_base_images.py
new file mode 100644
index 00000000000..7b04f6e6c87
--- /dev/null
+++ b/dev/breeze/tests/test_mirror_base_images.py
@@ -0,0 +1,98 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from subprocess import CompletedProcess
+from unittest import mock
+
+import pytest
+import requests
+from click.testing import CliRunner
+
+from airflow_breeze.commands.release_management_commands import (
+ get_latest_hardened_python_patchlevel,
+ mirror_base_images,
+)
+from airflow_breeze.global_constants import ALL_PYTHON_VERSION_TO_PATCHLEVEL_VERSION
+
+
+def _catalog_response(text: str) -> mock.MagicMock:
+ response = mock.MagicMock(spec=requests.Response)
+ response.text = text
+ return response
+
+
+@pytest.mark.parametrize(
+ ("definition", "expected"),
+ [
+ pytest.param(
+ "tags:\n - 3.14-debian12-dev\n - 3.14.8-debian12-dev\n", "3.14.8", id="patchlevel-tag"
+ ),
+ pytest.param(
+ "tags:\n - 3.12.9-debian12-dev\n - 3.12.15-debian12-dev\n", "3.12.15", id="numeric-max"
+ ),
+ pytest.param("tags:\n - 3.14-debian12-dev\n", None, id="no-patchlevel-tag"),
+ ],
+)
+def test_latest_hardened_python_patchlevel_is_read_from_the_catalog(definition, expected):
+ with mock.patch("requests.get", autospec=True, return_value=_catalog_response(definition)) as get:
+ assert get_latest_hardened_python_patchlevel("3.14", "bookworm") == expected
+ assert get.call_args.args[0].endswith("/image/python/debian-12/3.14-dev.yaml")
+
+
+def test_latest_hardened_python_patchlevel_defaults_to_trixie():
+ definition = "tags:\n - 3.14.8-debian12-dev\n - 3.14.9-debian13-dev\n"
+ with mock.patch("requests.get", autospec=True, return_value=_catalog_response(definition)) as get:
+ assert get_latest_hardened_python_patchlevel("3.14") == "3.14.9"
+ assert get.call_args.args[0].endswith("/image/python/debian-13/3.14-dev.yaml")
+
+
+def test_latest_hardened_python_patchlevel_is_none_when_the_catalog_is_unreachable():
+ with mock.patch("requests.get", autospec=True, side_effect=requests.ConnectionError("down")):
+ assert get_latest_hardened_python_patchlevel("3.14") is None
+
+
+@pytest.mark.parametrize("newer_available", [True, False])
+def test_mirror_also_copies_a_newer_published_patchlevel(newer_available):
+ pinned = ALL_PYTHON_VERSION_TO_PATCHLEVEL_VERSION["3.14"]
+ latest = "3.14.99" if newer_available else pinned
+ with (
+ mock.patch(
+ "airflow_breeze.commands.release_management_commands.get_latest_hardened_python_patchlevel",
+ autospec=True,
+ return_value=latest,
+ ),
+ mock.patch(
+ "airflow_breeze.commands.release_management_commands.run_command",
+ autospec=True,
+ return_value=CompletedProcess(args=[], returncode=0),
+ ) as run,
+ ):
+ result = CliRunner().invoke(mirror_base_images, ["--python", "3.14"], catch_exceptions=False)
+
+ assert result.exit_code == 0
+ sources = [call.args[0][-1] for call in run.call_args_list]
+ if newer_available:
+ assert sources == [
+ f"dhi.io/python:{pinned}-debian13-dev",
+ "dhi.io/python:3.14.99-debian13-dev",
+ f"dhi.io/python:{pinned}-debian12-dev",
+ "dhi.io/python:3.14.99-debian12-dev",
+ ]
+ assert "ghcr.io/apache/airflow/base/python:3.14.99-debian13-dev" in run.call_args_list[1].args[0]
+ else:
+ assert sources == [f"dhi.io/python:{pinned}-debian13-dev", f"dhi.io/python:{pinned}-debian12-dev"]
diff --git a/docker-stack-docs/build-arg-ref.rst b/docker-stack-docs/build-arg-ref.rst
index 444c615cb50..0164efba611 100644
--- a/docker-stack-docs/build-arg-ref.rst
+++ b/docker-stack-docs/build-arg-ref.rst
@@ -32,7 +32,22 @@ Those are the most common arguments that you use when you want to build a custom
+==========================================+===========================================+=============================================+
| ``AIRFLOW_VERSION`` | :subst-code:`|airflow-version|` | Version of Airflow. |
+------------------------------------------+-------------------------------------------+---------------------------------------------+
-| ``AIRFLOW_PYTHON_VERSION`` | ``3.13.16`` | Version of Python. |
+| ``AIRFLOW_IMAGE_FLAVOR`` | ``hardened`` | ``hardened`` takes Python from |
+| | | ``BASE_IMAGE``. ``legacy`` compiles |
+| | | ``AIRFLOW_PYTHON_VERSION`` from sources on |
+| | | a plain Debian ``BASE_IMAGE`` (see below |
+| | | the table). |
++------------------------------------------+-------------------------------------------+---------------------------------------------+
+| ``AIRFLOW_PYTHON_VERSION`` | ``3.13.16`` | Python version recorded in the image |
+| | | labels. In the hardened flavor the Python |
+| | | itself comes from ``BASE_IMAGE``, so change |
+| | | both together. In the legacy flavor it is |
+| | | the version compiled from sources. |
++------------------------------------------+-------------------------------------------+---------------------------------------------+
+| ``BASE_IMAGE`` | (see below the table) | Airflow's public mirror of the Docker |
+| | | Hardened Image that Python comes from. |
+| | | Point it at a ``dhi.io`` ``-fips-`` variant |
+| | | to build a FIPS-compliant image. |
+------------------------------------------+-------------------------------------------+---------------------------------------------+
| ``AIRFLOW_EXTRAS`` | (see below the table) | Default extras with which Airflow is |
| | | installed. |
@@ -81,6 +96,19 @@ Those are the most common arguments that you use when you want to build a custom
Before Airflow 2.2, the image also had ``AIRFLOW_GID`` parameter, but it did not provide any additional
functionality - only added confusion - so it has been removed.
+The default ``BASE_IMAGE`` is Airflow's public mirror of the `Docker Hardened Image <https://dhi.io>`_ for
+Python, tagged with the same Python version as ``AIRFLOW_PYTHON_VERSION`` - for Airflow |airflow-version| that
+is ``ghcr.io/apache/airflow/base/python:3.13.16-debian13-dev``. Pulling the mirror needs no credentials;
+pulling ``dhi.io`` directly requires a ``docker login dhi.io``.
+
+For the 3.4.x line Airflow also publishes ``legacy`` images, built the way all images were built before
+3.4.0: Python is downloaded from python.org, its signature verified and compiled on a ``debian:bookworm-slim``
+base. They are deprecated and will be removed in Airflow 3.5.0. To build one yourself, set both arguments:
+
+.. code-block:: bash
+
+ docker build . --build-arg AIRFLOW_IMAGE_FLAVOR="legacy" --build-arg BASE_IMAGE="debian:bookworm-slim"
+
List of default extras in the production Dockerfile:
.. BEGINNING OF EXTRAS LIST UPDATED BY PRE COMMIT
diff --git a/docker-stack-docs/build.rst b/docker-stack-docs/build.rst
index 15e7af68f4b..24496484d6d 100644
--- a/docker-stack-docs/build.rst
+++ b/docker-stack-docs/build.rst
@@ -884,26 +884,136 @@ vetted by the security teams. It is also the most complex way of building the im
expert of building and using Dockerfiles in order to use it and have to have specific needs of security if
you want to follow that route.
+.. _image-hardened-base:
+
+Properties of the hardened base image
+.....................................
+
+Since Airflow 3.4.0 the images are built on the Debian 13 "trixie" ``-dev`` variant of the
+`Docker Hardened Image <https://dhi.io>`_ for Python (``dhi.io/python``, mirrored to
+``ghcr.io/apache/airflow/base/python``) instead of compiling Python on ``debian:bookworm-slim``. Docker
+publishes the definitions these images are built from in the
+`docker-hardened-images/catalog <https://github.com/docker-hardened-images/catalog>`_ repository
+(``package/python`` for the Python build, ``image/python`` for the image). The differences from the
+previous images that can matter when you extend or run the image are:
+
+* **Python is patched.** Docker applies its own patches on top of the upstream CPython release
+ (``package/python/patch`` in the catalog). One of them makes ``pkgutil.get_data()`` reject resource
+ paths that are absolute or contain ``..`` (CVE-2026-3479) - upstream CPython only documents that
+ restriction and does not enforce it. Libraries that load bundled data through ``../`` paths fail with
+ ``ValueError: resource must be a relative path with no parent directory components``; for example
+ ``moto`` before 5.2.2.
+* **Threads get a 1 MiB stack.** The hardened Python is compiled with
+ ``-DTHREAD_STACK_SIZE=0x100000``, while the previous images used the ``glibc`` default (the 8 MiB
+ ``ulimit -s``). On Python 3.12 and 3.13 the interpreter's C recursion guard is a fixed depth that
+ assumes the larger stack, so deeply nested data - for example a JSON body parsed in a web server worker
+ thread - can crash the process instead of raising ``RecursionError``. The image restores the 8 MiB
+ default for threads started by Python with a ``airflow-thread-stack-size.pth`` file in the base
+ Python's ``site-packages``; delete that file if you need the smaller stacks.
+* **Python is built without profile-guided optimization.** The previous images were built with
+ ``--enable-optimizations`` and ``--with-lto``; the hardened Python uses only ``--with-lto``, so CPU-bound Python code
+ can run measurably slower.
+* **Python lives under** ``/usr``. The Debian 13 hardened images install Python as Debian packages
+ (``python-3.13``, ``libpython-3.13``, ...), so it is ``/usr/bin/python3`` with the standard library in
+ ``/usr/lib/python3.13`` (the Debian 12 ones used ``/opt/python``). ``/usr/python`` and the
+ ``/usr/local/bin`` links point to it, so the paths used before keep working. The standard library ships without ``.pyc`` files, so the build
+ compiles it into the image.
+* **The OS is minimal.** The base ships no compiler, ``curl``, ``wget``, ``git``, ``gzip``, ``which`` or
+ ``ldconfig``, no ``dash`` (``/bin/sh`` is ``bash``) and a stripped ``/etc`` - for example without
+ ``/etc/shells`` or the PAM ``common-*`` files. The build restores what Airflow and the Debian packages
+ it installs need; a custom image that relied on something else being present has to install it.
+* **Packages also come from Docker's Debian repository.** Next to Debian's own repositories, the Debian 13
+ images configure ``http://dhi.io/deb/debian/main`` (``/etc/apt/sources.list.d/dhi.sources``, signed with
+ ``/usr/share/keyrings/dhi-deb-main.gpg``), which serves Docker's rebuilds of Debian packages. Their
+ versions carry a ``+dhi<N>`` suffix, so apt prefers them over Debian's builds - in the image and in
+ images extending it. The repository can be used anonymously: unlike pulling the ``dhi.io`` images,
+ installing packages from it needs no ``docker login`` or any other credentials.
+* **Installing apt packages can pull in a second Python.** The Debian 13 images register their own Python
+ with ``dpkg`` (``python-3.12``, ``libpython-3.12``, ...), and Docker's package repository serves the
+ other Python versions as packages too. A Debian package that depends on ``python3`` - directly or, as
+ ``libenchant-2-dev`` does, through its build tools - therefore makes apt install another hardened Python
+ version (for example ``python-3.13`` into a Python 3.12 image), and on the Debian 12 images it pulls in
+ Debian's own ``python3``. The second interpreter does not replace the image's Python, but it can take
+ over ``/usr/bin/python3`` and its headers, and C extensions built afterwards then compile against the
+ wrong Python. The image build fails when this happens, but a custom image that installs extra apt
+ packages is not checked. Check what ``apt-get install --simulate`` would install before adding packages
+ that might depend on ``python3``, and prefer the runtime library packages over their ``-dev`` variants.
+* **The image's Python packages are held.** On the Debian 13 images, Debian's own ``python3`` packages
+ conflict with the image's ``python-3.X`` packages, so a package that apt can only satisfy with them -
+ for example NodeSource's ``nodejs`` - would make apt remove the image's Python without asking. The
+ image marks its Python packages with ``apt-mark hold``, so apt reports the ``python3`` conflict and the
+ installation fails instead. Install that software some other way - for Node.js, copy the ``node`` binary
+ from the official ``node`` image - rather than releasing the hold.
+* **C extensions are compiled more strictly.** The Debian 13 images come with GCC 14, which turns
+ incompatible pointer types, integer conversions and implicit function declarations into errors, and the
+ hardened Python passes ``-Werror=implicit-function-declaration`` on to every C extension it builds. Old
+ source distributions that compiled with warnings before - for example ``xmlsec`` 1.3.14 or ``ibm-db``
+ 3.2.0 - fail to build. Prefer versions with wheels or newer releases; if you have to build such a
+ package, setting ``CFLAGS="-Wno-error=incompatible-pointer-types -Wno-error=int-conversion
+ -Wno-error=implicit-function-declaration"`` for the installation turns the errors back into warnings.
+* **apt does not refresh the shared library cache.** The base images ship ``libc-bin`` without its
+ ``dpkg`` trigger, so installing a shared library does not run ``ldconfig``, and anything that looks
+ libraries up through the cache - such as ``ctypes.util.find_library()`` - does not find it. The image
+ adds ``/etc/apt/apt.conf.d/99airflow-ldconfig``, which runs ``ldconfig`` after every ``dpkg`` run,
+ including those of images extending it.
+* **Some OS files are modified.** ``/etc/os-release`` identifies the system as "Docker Hardened Images
+ (Debian)", and ``/etc/debian_version`` differs from the Debian package's copy, so an ``apt-get upgrade``
+ that touches ``base-files`` would stop at ``dpkg``'s interactive configuration-file prompt. The image
+ ships ``/etc/dpkg/dpkg.cfg.d/airflow-keep-conffiles`` (``force-confdef`` and ``force-confold``), so
+ ``dpkg`` keeps the image's copy of a configuration file without asking - also in images that extend
+ it. A package upgrade therefore does not replace a configuration file that differs from the
+ package's version.
+* **The tags are rebuilt in place.** Docker republishes the same tags as CVEs are fixed, and Airflow
+ refreshes its mirror weekly, so rebuilding an image from the same ``BASE_IMAGE`` tag can pick up a newer
+ base. The weekly mirror also copies the newest Python patch release Docker publishes, and Airflow's
+ regular dependency upgrade moves the pinned patch level to it, so the images follow Docker's releases
+ rather than python.org's.
+
+.. _image-legacy-flavor:
+
+Debian trixie and the legacy image flavor
+.........................................
+
+Together with the hardened base, the default images moved from Debian 12 "bookworm" to Debian 13
+"trixie". Besides the newer system libraries, this shows up in two places when you extend the image:
+
+* Several library packages were renamed in trixie's 64-bit ``time_t`` transition - for example
+ ``libssl3`` is ``libssl3t64`` and ``libldap-2.5-0`` is ``libldap2`` - so ``RUNTIME_APT_DEPS`` or
+ ``ADDITIONAL_RUNTIME_APT_DEPS`` that name the bookworm packages have to be updated.
+* The MySQL-compatible client comes from MariaDB 11.8, the first MariaDB LTS published for trixie. The
+ ``mysql`` named commands are provided by ``mariadb-client-compat``.
+
+For the 3.4.x line Airflow also publishes ``legacy`` images, built as before 3.4.0: Python downloaded
+from python.org, verified with its sigstore signature and compiled from sources on ``debian:bookworm-slim``.
+They are deprecated and will be removed in Airflow 3.5.0. You can build one with the
+``AIRFLOW_IMAGE_FLAVOR`` build argument:
+
+.. code-block:: bash
+
+ docker build . \
+ --build-arg AIRFLOW_IMAGE_FLAVOR="legacy" \
+ --build-arg BASE_IMAGE="debian:bookworm-slim" \
+ --tag my-image:my-tag
+
+The flavor an image was built with is recorded in its ``org.apache.airflow.image.flavor`` label.
+
.. _image-build-fips:
Build images in FIPS-compliant environments
...........................................
-If you are building images in a FIPS-compliant environment, you might encounter issues with the default
-build process. For example, the default build process uses ``--with-lto`` (Link Time Optimization) when
-building Python, which might fail in FIPS mode because LTO uses MD5 checksums to verify object files
-during compilation, and MD5 is blocked in FIPS mode.
-
-In order to build the image in FIPS-compliant environment, you can use ``PYTHON_LTO`` build argument
-and set it to ``false``.
+Airflow images are based on the `Docker Hardened Images <https://dhi.io>`_ for Python, and Docker
+publishes FIPS-validated variants of those images. Those variants are only available with a paid
+Docker subscription, so they cannot be the default, but you can point the build at one with the
+``BASE_IMAGE`` build argument.
.. code-block:: bash
- docker build . --build-arg PYTHON_LTO="false" --tag my-image:my-tag
+ docker build . --build-arg BASE_IMAGE="dhi.io/python:3.13.16-debian13-fips-dev" --tag my-image:my-tag
.. note::
- While disabling LTO is necessary for FIPS compliance during the build process, it is not sufficient
+ While building on a FIPS-validated base image is necessary for FIPS compliance, it is not sufficient
to make the image fully FIPS compliant. There might be other reasons for FIPS incompatibility
(for example usage of non-FIPS compliant algorithms in the software installed in the image).
You should verify the compliance of the image yourself.
diff --git a/docker-stack-docs/changelog.rst b/docker-stack-docs/changelog.rst
index 78f9c748475..79454aa9a66 100644
--- a/docker-stack-docs/changelog.rst
+++ b/docker-stack-docs/changelog.rst
@@ -43,6 +43,49 @@ Airflow 3.4.0
* The ``tdsodbc`` package was added to the image so that the FreeTDS ODBC driver
(``libtdsodbc.so``) is available for connecting to Sybase/TDS databases via ODBC.
+ * The ``libxmlsec1-openssl`` package was added to the image. ``libxmlsec1`` contains no crypto
+ engine of its own, so ``import xmlsec`` (used by ``python3-saml``) failed with
+ ``libxmlsec1-openssl.so.1: cannot open shared object file``.
+
+In Airflow 3.4.0 the base image changed again - from ``debian:bookworm-slim`` with a Python compiled in
+the image, to the Debian 13 "trixie" `Docker Hardened Image <https://dhi.io>`_ for Python, which already
+carries a Python built by Docker. This removes the Python compilation, and the source download and signature verification
+that went with it, from the build entirely - the OS dependency layer of the production image went from
+roughly 210s to 105s in a local cache-disabled build.
+
+* The ``BASE_IMAGE`` arg now defaults to ``ghcr.io/apache/airflow/base/python:<version>-debian13-dev``,
+ which is Airflow's public mirror of the upstream ``dhi.io/python`` image. Pulling from ``dhi.io``
+ directly requires a ``docker login dhi.io``; pulling the mirror requires nothing. If you pass your own
+ ``BASE_IMAGE``, it now has to be an image that already provides Python - a bare ``debian:bookworm-slim``
+ only works with ``AIRFLOW_IMAGE_FLAVOR="legacy"``.
+* The image is based on Debian 13 "trixie" instead of Debian 12 "bookworm". Library packages renamed in
+ trixie's 64-bit ``time_t`` transition (for example ``libssl3t64``, ``libldap2``) have to be used in
+ custom ``RUNTIME_APT_DEPS``, and the MySQL-compatible client comes from MariaDB 11.8 (with
+ ``mariadb-client-compat`` providing the ``mysql`` named commands).
+* The new ``AIRFLOW_IMAGE_FLAVOR`` arg (``hardened`` by default) can be set to ``legacy`` to build the
+ image as before: Python compiled from sources on a plain Debian ``BASE_IMAGE`` such as
+ ``debian:bookworm-slim``. Images of that flavor are published for the 3.4.x line, are deprecated, and
+ will be removed in Airflow 3.5.0. The flavor is recorded in the ``org.apache.airflow.image.flavor`` label.
+* The ``PYTHON_LTO`` arg was removed. It only controlled Link-Time Optimization while compiling Python;
+ the legacy flavor always compiles with LTO. To build a FIPS-compliant image, point ``BASE_IMAGE`` at a FIPS
+ variant of the hardened image, for example ``dhi.io/python:3.13.16-debian13-fips-dev`` (those variants
+ require a paid Docker subscription).
+* Python is installed under ``/usr`` by the base image, as Debian packages. ``/usr/python`` is a symlink to it, and the
+ ``/usr/local/bin`` symlinks are unchanged, so paths that worked before keep working.
+* The hardened images ship the standard library with no ``.pyc`` files, so the build compiles it into the
+ image. This keeps the behaviour introduced in Airflow 3.1.4 (see below): the standard library is owned
+ by root while the image runs as ``airflow``, so a missing bytecode cache could never be filled and every
+ import would leak a negative ``dentry``.
+* Python in the hardened images is not the plain upstream build: Docker patches it (``pkgutil.get_data()``
+ rejects ``..`` resource paths), builds it without profile-guided optimization, and compiles it with a
+ 1 MiB default thread stack. The image restores the previous 8 MiB thread stack. See
+ :ref:`image-hardened-base` for these and the other differences from the previous images.
+
+As with any base image change, some ``apt`` packages that used to be present as a side effect are not
+there any more. The hardened images are deliberately minimal - they ship no compiler, no ``curl``,
+``wget``, ``git`` or ``gzip``, and a stripped ``/etc`` - so a custom image that relied on something being
+present may need to install it explicitly. See :doc:`Building the image <build>`.
+
Airflow 3.1.4
~~~~~~~~~~~~~
diff --git a/docker-stack-docs/docker-examples/customizing/add-build-essential-custom.sh b/docker-stack-docs/docker-examples/customizing/add-build-essential-custom.sh
index 32882f2a3d6..e211cd5e2dc 100755
--- a/docker-stack-docs/docker-examples/customizing/add-build-essential-custom.sh
+++ b/docker-stack-docs/docker-examples/customizing/add-build-essential-custom.sh
@@ -31,7 +31,7 @@ export DOCKER_BUILDKIT=1
docker build . \
--pull \
- --build-arg BASE_IMAGE="debian:bookworm-slim" \
+ --build-arg BASE_IMAGE="ghcr.io/apache/airflow/base/python:3.13.16-debian13-dev" \
--build-arg AIRFLOW_VERSION="${AIRFLOW_VERSION}" \
--build-arg AIRFLOW_PYTHON_VERSION="3.13.16" \
--build-arg ADDITIONAL_PYTHON_DEPS="mpi4py==4.1.1" \
diff --git a/docker-stack-docs/docker-examples/customizing/custom-sources.sh b/docker-stack-docs/docker-examples/customizing/custom-sources.sh
index 83dc4aed7c7..cae38a28fa9 100755
--- a/docker-stack-docs/docker-examples/customizing/custom-sources.sh
+++ b/docker-stack-docs/docker-examples/customizing/custom-sources.sh
@@ -32,7 +32,7 @@ export DOCKER_BUILDKIT=1
docker build . -f Dockerfile \
--pull \
--platform 'linux/amd64' \
- --build-arg BASE_IMAGE="debian:bookworm-slim" \
+ --build-arg BASE_IMAGE="ghcr.io/apache/airflow/base/python:3.13.16-debian13-dev" \
--build-arg AIRFLOW_PYTHON_VERSION="3.13.16" \
--build-arg AIRFLOW_VERSION="${AIRFLOW_VERSION}" \
--build-arg ADDITIONAL_AIRFLOW_EXTRAS="slack,odbc" \
diff --git a/docker-stack-docs/docker-examples/customizing/pypi-dev-runtime-deps.sh b/docker-stack-docs/docker-examples/customizing/pypi-dev-runtime-deps.sh
index 21682e1adbd..0687a3deb94 100755
--- a/docker-stack-docs/docker-examples/customizing/pypi-dev-runtime-deps.sh
+++ b/docker-stack-docs/docker-examples/customizing/pypi-dev-runtime-deps.sh
@@ -32,7 +32,7 @@ export DOCKER_BUILDKIT=1
docker build . \
--pull \
- --build-arg BASE_IMAGE="debian:bookworm-slim" \
+ --build-arg BASE_IMAGE="ghcr.io/apache/airflow/base/python:3.13.16-debian13-dev" \
--build-arg AIRFLOW_PYTHON_VERSION="3.13.16" \
--build-arg AIRFLOW_VERSION="${AIRFLOW_VERSION}" \
--build-arg ADDITIONAL_AIRFLOW_EXTRAS="jdbc" \
diff --git a/docker-stack-docs/docker-examples/customizing/pypi-extras-and-deps.sh b/docker-stack-docs/docker-examples/customizing/pypi-extras-and-deps.sh
index 7f3b53337b6..c5ce80d6141 100755
--- a/docker-stack-docs/docker-examples/customizing/pypi-extras-and-deps.sh
+++ b/docker-stack-docs/docker-examples/customizing/pypi-extras-and-deps.sh
@@ -31,7 +31,7 @@ export DOCKER_BUILDKIT=1
docker build . \
--pull \
- --build-arg BASE_IMAGE="debian:bookworm-slim" \
+ --build-arg BASE_IMAGE="ghcr.io/apache/airflow/base/python:3.13.16-debian13-dev" \
--build-arg AIRFLOW_PYTHON_VERSION="3.13.16" \
--build-arg AIRFLOW_VERSION="${AIRFLOW_VERSION}" \
--build-arg ADDITIONAL_AIRFLOW_EXTRAS="mssql,hdfs" \
diff --git a/docker-stack-docs/docker-examples/customizing/pypi-selected-version.sh b/docker-stack-docs/docker-examples/customizing/pypi-selected-version.sh
index 009fa3d58f4..0e7e256b3a8 100755
--- a/docker-stack-docs/docker-examples/customizing/pypi-selected-version.sh
+++ b/docker-stack-docs/docker-examples/customizing/pypi-selected-version.sh
@@ -30,7 +30,7 @@ export AIRFLOW_VERSION=3.0.3
export DOCKER_BUILDKIT=1
docker build . \
- --build-arg BASE_IMAGE="debian:bookworm-slim" \
+ --build-arg BASE_IMAGE="ghcr.io/apache/airflow/base/python:3.13.16-debian13-dev" \
--build-arg AIRFLOW_PYTHON_VERSION="3.13.16" \
--build-arg AIRFLOW_VERSION="${AIRFLOW_VERSION}" \
--tag "my-pypi-selected-version:0.0.1"
diff --git a/docker-stack-docs/docker-examples/extending/add-providers/Dockerfile b/docker-stack-docs/docker-examples/extending/add-providers/Dockerfile
index d1eedfcb6c6..bcdf0b73bf6 100644
--- a/docker-stack-docs/docker-examples/extending/add-providers/Dockerfile
+++ b/docker-stack-docs/docker-examples/extending/add-providers/Dockerfile
@@ -21,11 +21,11 @@ FROM apache/airflow:3.4.0
USER root
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
- openjdk-17-jre-headless \
+ openjdk-21-jre-headless \
&& apt-get autoremove -yqq --purge \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
USER airflow
-ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
+ENV JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
RUN pip install --no-cache-dir "apache-airflow==${AIRFLOW_VERSION}" apache-airflow-providers-apache-spark==5.1.1
# [END Dockerfile]
diff --git a/docker-stack-docs/docker-examples/restricted/restricted_environments.sh b/docker-stack-docs/docker-examples/restricted/restricted_environments.sh
index 237f5ce4291..be15b9e2096 100755
--- a/docker-stack-docs/docker-examples/restricted/restricted_environments.sh
+++ b/docker-stack-docs/docker-examples/restricted/restricted_environments.sh
@@ -47,7 +47,7 @@ export DOCKER_BUILDKIT=1
docker build . \
--pull \
- --build-arg BASE_IMAGE="debian:bookworm-slim" \
+ --build-arg BASE_IMAGE="ghcr.io/apache/airflow/base/python:3.13.16-debian13-dev" \
--build-arg AIRFLOW_PYTHON_VERSION="3.13.16" \
--build-arg AIRFLOW_INSTALLATION_METHOD="apache-airflow" \
--build-arg AIRFLOW_VERSION="${AIRFLOW_VERSION}" \
diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt
index 029b8e21ba4..a0198beb322 100644
--- a/docs/spelling_wordlist.txt
+++ b/docs/spelling_wordlist.txt
@@ -344,6 +344,7 @@ cosmosdb
cp
cpu
cpus
+CPython
CRD
crd
CreateQueryOperator
@@ -1613,6 +1614,7 @@ Sharepoint
shm
shouldn
sie
+sigstore
SIGTERM
sku
sla
@@ -1854,6 +1856,7 @@ triaging
triggerer
triggerers
trino
+trixie
trojan
TrustedRouter
truthy
diff --git a/kubernetes-tests/lang_sdk/Dockerfile.runtimes b/kubernetes-tests/lang_sdk/Dockerfile.runtimes
index 00bd97fc6f5..7b1883bdce4 100644
--- a/kubernetes-tests/lang_sdk/Dockerfile.runtimes
+++ b/kubernetes-tests/lang_sdk/Dockerfile.runtimes
@@ -23,7 +23,14 @@
# both runtimes to parse native Java and TypeScript Dags, and using one image
# everywhere keeps the pod templates and the Helm image override identical
# across components.
+#
+# Node comes as the official binary rather than a deb: the NodeSource nodejs
+# package depends on python3, which apt would satisfy on the hardened images by
+# replacing the image's own Python with Debian's.
ARG BASE_IMAGE
+ARG NODE_IMAGE="node:22-bookworm-slim"
+FROM ${NODE_IMAGE} AS node
+
FROM ${BASE_IMAGE}
USER root
@@ -31,19 +38,5 @@ RUN apt-get update \
&& apt-get install --no-install-recommends -y default-jre-headless \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
-# The keyring step pipes curl into gpg, so a failure mid-pipe has to fail the build.
-SHELL ["/bin/bash", "-o", "pipefail", "-c"]
-RUN apt-get update \
- && apt-get install --no-install-recommends -y ca-certificates curl gnupg \
- && mkdir -p /etc/apt/keyrings \
- && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
- | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
- && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" \
- > /etc/apt/sources.list.d/nodesource.list \
- && apt-get update \
- && apt-get install --no-install-recommends -y nodejs \
- && apt-get purge -y --auto-remove gnupg \
- && apt-get clean \
- && rm -rf /var/lib/apt/lists/*
-SHELL ["/bin/sh", "-c"]
+COPY --from=node /usr/local/bin/node /usr/local/bin/node
USER airflow
diff --git a/scripts/ci/prek/update_docker_gpg_keys.py b/scripts/ci/prek/update_docker_gpg_keys.py
index 37eee4e1e32..30c7e93b9d6 100755
--- a/scripts/ci/prek/update_docker_gpg_keys.py
+++ b/scripts/ci/prek/update_docker_gpg_keys.py
@@ -41,6 +41,8 @@ KEYS: dict[str, str] = {
"postgres": "7FCC7D46ACCC4CF8",
# Microsoft APT repository signing key (MSSQL ODBC)
"microsoft": "EB3E94ADBE1229CF",
+ # Microsoft signs the Debian 13 (trixie) repository with its newer general signing key
+ "microsoft-2025": "EE4D7792F748182B",
}
diff --git a/scripts/ci/prek/upgrade_important_versions.py b/scripts/ci/prek/upgrade_important_versions.py
index 987d4f61449..6cbe86071d9 100755
--- a/scripts/ci/prek/upgrade_important_versions.py
+++ b/scripts/ci/prek/upgrade_important_versions.py
@@ -401,53 +401,41 @@ def prune_obsolete_cooldown_overrides() -> bool:
return True
-def get_all_python_versions() -> list[Version]:
- """
- Fetch all released Python versions by parsing the Python FTP directory listing.
- This provides static information about all available Python releases.
- """
- if VERBOSE:
- console.print("[bright_blue]Fetching all released Python versions from python.org FTP")
- url = "https://www.python.org/ftp/python/"
- headers = {"User-Agent": "Python requests"}
- response = requests.get(url, headers=headers)
- response.raise_for_status()
-
- # Parse the HTML directory listing to extract version numbers
- # The FTP directory listing has links like: <a href="3.12.1/">3.12.1/</a>
- versions = []
- # Match version patterns like "3.12.1/" in href attributes
- version_pattern = re.compile(r'href="(\d+\.\d+\.\d+)/"')
-
- for match in version_pattern.finditer(response.text):
- version_str = match.group(1)
- try:
- # Parse as version to validate it's a proper version number
- version_obj = Version(version_str)
- # Only include Python 3.x versions
- if version_obj.major == 3:
- versions.append(version_obj)
- except Exception:
- # Skip invalid version strings
- continue
-
- return versions
+HARDENED_PYTHON_CATALOG_URL = (
+ "https://raw.githubusercontent.com/docker-hardened-images/catalog/main/image/python/debian-{debian}/"
+ "{python_major_minor}-dev.yaml"
+)
+# One patchlevel pin serves the images of every Debian release Airflow builds on (trixie is the
+# default, bookworm is still available for PROD images).
+HARDENED_PYTHON_DEBIAN_RELEASES = ["13", "12"]
-def get_latest_python_version(python_major_minor: str, all_versions: list[Version]) -> str:
+def get_latest_python_version(python_major_minor: str) -> str:
"""
- Fetch the latest released Python version for a given major.minor (e.g. '3.12') from FTP directory listing.
- Uses static directory information rather than API calls.
+ Return the newest Python patchlevel published as a Docker Hardened Image for a major.minor.
+
+ The images are built on those hardened images, so the pinned patchlevel has to be one Docker has
+ published - a newer python.org release is of no use until Docker builds it. The image definitions
+ in the public catalog list the patchlevel tag each image is currently built from.
"""
- # Only consider releases matching the major.minor.patch pattern
- matching = [
- version for version in all_versions if python_major_minor == f"{version.major}.{version.minor}"
- ]
- if not matching:
- console.print(f"[bright_red]No released Python versions found for {python_major_minor}")
- sys.exit(1)
- # Sort and return the latest version
- latest_version = sorted(matching)[-1]
+ latest_per_release = []
+ for debian in HARDENED_PYTHON_DEBIAN_RELEASES:
+ url = HARDENED_PYTHON_CATALOG_URL.format(debian=debian, python_major_minor=python_major_minor)
+ if VERBOSE:
+ console.print(
+ f"[bright_blue]Fetching the hardened Python {python_major_minor} definition from {url}"
+ )
+ response = requests.get(url, headers={"User-Agent": "Python requests"})
+ response.raise_for_status()
+ pattern = rf"^\s*-\s*(\d+\.\d+\.\d+)-debian{debian}-dev\s*$"
+ versions = [Version(tag) for tag in re.findall(pattern, response.text, re.MULTILINE)]
+ if not versions:
+ console.print(f"[bright_red]No hardened Python {python_major_minor} image tags found in {url}")
+ sys.exit(1)
+ latest_per_release.append(max(versions))
+ # Docker does not always publish a patchlevel for every Debian release on the same day - pinning
+ # one that a release lacks would point its builds at a tag that does not exist.
+ latest_version = min(latest_per_release)
if VERBOSE:
console.print(f"[bright_blue]Latest version for {python_major_minor}: {latest_version}")
return str(latest_version)
@@ -677,6 +665,11 @@ GOLANG_PATTERNS: list[tuple[re.Pattern, Quoting]] = [
AIRFLOW_IMAGE_PYTHON_PATTERNS: list[tuple[re.Pattern, Quoting]] = [
(re.compile(r"(AIRFLOW_PYTHON_VERSION=)(\"[0-9.abrc]+\")"), Quoting.DOUBLE_QUOTED),
+ # Base image tags pin the same Python patchlevel as AIRFLOW_PYTHON_VERSION and have to move with it
+ (
+ re.compile(r"((?:dhi\.io|ghcr\.io/apache/airflow/base)/python:)([0-9]+\.[0-9]+\.[0-9.abrc]+)"),
+ Quoting.UNQUOTED,
+ ),
(
re.compile(r"(\| ``AIRFLOW_PYTHON_VERSION`` *\| )(``[0-9.abrc]+``)( *\|)"),
Quoting.REVERSE_DOUBLE_QUOTED,
@@ -964,10 +957,9 @@ def fetch_python_versions() -> dict[str, str]:
if not UPGRADE_PYTHON:
return latest_python_versions
- all_python_versions = get_all_python_versions()
for python_major_minor_version in ALL_PYTHON_MAJOR_MINOR_VERSIONS:
latest_python_versions[python_major_minor_version] = get_latest_python_version(
- python_major_minor_version, all_python_versions
+ python_major_minor_version
)
if python_major_minor_version == DEFAULT_PROD_IMAGE_PYTHON_VERSION:
console.print(
diff --git a/scripts/docker/common.sh b/scripts/docker/common.sh
index d8ebb2e261f..b18c87098c5 100644
--- a/scripts/docker/common.sh
+++ b/scripts/docker/common.sh
@@ -136,12 +136,12 @@ function common::get_constraints_location() {
function common::show_packaging_tool_version_and_location() {
echo "PATH=${PATH}"
- echo "Installed pip: $(pip --version): $(which pip)"
+ echo "Installed pip: $(pip --version): $(command -v pip)"
if [[ ${PACKAGING_TOOL} == "pip" ]]; then
echo "${COLOR_BLUE}Using 'pip' to install Airflow${COLOR_RESET}"
else
echo "${COLOR_BLUE}Using 'uv' to install Airflow${COLOR_RESET}"
- echo "Installed uv: $(uv --version 2>/dev/null || echo "Not installed yet"): $(which uv 2>/dev/null)"
+ echo "Installed uv: $(uv --version 2>/dev/null || echo "Not installed yet"): $(command -v uv 2>/dev/null)"
fi
}
@@ -153,7 +153,7 @@ function common::install_packaging_tools() {
echo
else
echo
- echo "${COLOR_BLUE}Checking packaging tools for system Python installation: $(which python)${COLOR_RESET}"
+ echo "${COLOR_BLUE}Checking packaging tools for system Python installation: $(command -v python)${COLOR_RESET}"
echo
fi
if [[ ${AIRFLOW_PIP_VERSION=} == "" ]]; then
@@ -217,6 +217,16 @@ function common::install_packaging_tools() {
fi
}
+function common::debian_codename() {
+ # shellcheck disable=SC1091
+ (. /etc/os-release; printf '%s\n' "${VERSION_CODENAME}")
+}
+
+function common::debian_release() {
+ # shellcheck disable=SC1091
+ (. /etc/os-release; printf '%s\n' "${VERSION_ID}")
+}
+
function common::import_trusted_gpg() {
common::get_colors
diff --git a/scripts/docker/entrypoint_ci.sh b/scripts/docker/entrypoint_ci.sh
index 856466ad986..1e61d0bd856 100755
--- a/scripts/docker/entrypoint_ci.sh
+++ b/scripts/docker/entrypoint_ci.sh
@@ -200,8 +200,9 @@ SSH_CONFIG
sudo service ssh restart >/dev/null 2>&1
- # Sometimes the server is not quick enough to load the keys!
- while [[ $(ssh-keyscan -H localhost 2>/dev/null | wc -l) != "3" ]] ; do
+ # Sometimes the server is not quick enough to load the keys! OpenSSH 10 also prints a
+ # "# localhost:22 SSH-2.0-..." comment line per connection to stdout, so count only the keys.
+ while [[ $(ssh-keyscan -H localhost 2>/dev/null | grep -cv '^#') != "3" ]] ; do
echo "Not all keys yet loaded by the server"
sleep 0.05
done
diff --git a/scripts/docker/install_mssql.sh b/scripts/docker/install_mssql.sh
index f50f213d851..b1786006114 100644
--- a/scripts/docker/install_mssql.sh
+++ b/scripts/docker/install_mssql.sh
@@ -38,12 +38,14 @@ function install_mssql_client() {
packages=("msodbcsql18")
common::import_trusted_gpg "EB3E94ADBE1229CF" "microsoft"
+ # The Debian 13 repository is signed with Microsoft's newer key, the older releases with the one above.
+ common::import_trusted_gpg "EE4D7792F748182B" "microsoft-2025"
echo
echo "${COLOR_BLUE}Installing mssql client${COLOR_RESET}"
echo
- echo "deb [arch=amd64,arm64] https://packages.microsoft.com/debian/$(lsb_release -rs)/prod $(lsb_release -cs) main" > \
+ echo "deb [arch=amd64,arm64] https://packages.microsoft.com/debian/$(common::debian_release)/prod $(common::debian_codename) main" > \
/etc/apt/sources.list.d/mssql-release.list &&
mkdir -p /opt/microsoft/msodbcsql18 &&
touch /opt/microsoft/msodbcsql18/ACCEPT_EULA &&
diff --git a/scripts/docker/install_mysql.sh b/scripts/docker/install_mysql.sh
index 012ef9f14ac..e92a36a30e7 100644
--- a/scripts/docker/install_mysql.sh
+++ b/scripts/docker/install_mysql.sh
@@ -24,8 +24,6 @@ set -euo pipefail
common::get_colors
declare -a packages
-# https://mariadb.org/about/#maintenance-policy
-readonly MARIADB_LTS_VERSION="10.11"
: "${INSTALL_MYSQL_CLIENT:?Should be true or false}"
: "${INSTALL_MYSQL_CLIENT_TYPE:-mariadb}"
@@ -79,6 +77,13 @@ retry() {
}
install_mariadb_client() {
+ # https://mariadb.org/about/#maintenance-policy
+ # MariaDB publishes each LTS series only for the Debian releases current when it came out - 10.11
+ # has no trixie repository and 11.8 is the first LTS that does.
+ local mariadb_lts_version="10.11"
+ if [[ "$(common::debian_codename)" != "bookworm" ]]; then
+ mariadb_lts_version="11.8"
+ fi
# List of compatible package Oracle MySQL -> MariaDB:
# `mysql-client` -> `mariadb-client` or `mariadb-client-compat` (11+)
# `libmysqlclientXX` (where XX is a number) -> `libmariadb3-compat`
@@ -98,15 +103,19 @@ install_mariadb_client() {
echo
exit 1
fi
+ if [[ "${mariadb_lts_version%%.*}" -ge 11 ]]; then
+ # From 11.0 mariadb-client no longer ships the "mysql" named commands
+ packages+=("mariadb-client-compat")
+ fi
common::import_trusted_gpg "0xF1656F24C74CD1D8" "mariadb"
echo
- echo "${COLOR_BLUE}Installing MariaDB client version ${MARIADB_LTS_VERSION}: ${1}${COLOR_RESET}"
+ echo "${COLOR_BLUE}Installing MariaDB client version ${mariadb_lts_version}: ${1}${COLOR_RESET}"
echo "${COLOR_YELLOW}MariaDB client protocol-compatible with MySQL client.${COLOR_RESET}"
echo
- echo "deb [arch=amd64,arm64] https://archive.mariadb.org/mariadb-${MARIADB_LTS_VERSION}/repo/debian/ $(lsb_release -cs) main" > \
+ echo "deb [arch=amd64,arm64] https://archive.mariadb.org/mariadb-${mariadb_lts_version}/repo/debian/ $(common::debian_codename) main" > \
/etc/apt/sources.list.d/mariadb.list
# Make sure that dependencies from MariaDB repo are preferred over Debian dependencies
printf "Package: *\nPin: release o=MariaDB\nPin-Priority: 999\n" > /etc/apt/preferences.d/mariadb
diff --git a/scripts/docker/install_os_dependencies.sh b/scripts/docker/install_os_dependencies.sh
index 7db52ba803c..681f6950ce8 100644
--- a/scripts/docker/install_os_dependencies.sh
+++ b/scripts/docker/install_os_dependencies.sh
@@ -25,8 +25,6 @@ if [[ "$#" != 1 ]]; then
exit 1
fi
-AIRFLOW_PYTHON_VERSION=${AIRFLOW_PYTHON_VERSION:-3.11.16}
-PYTHON_LTO=${PYTHON_LTO:-true}
GOLANG_MAJOR_MINOR_VERSION=${GOLANG_MAJOR_MINOR_VERSION:-1.24.4}
TEMURIN_VERSION=${TEMURIN_VERSION:-11}
NODEJS_VERSION=${NODEJS_VERSION:-22.23.1}
@@ -35,7 +33,25 @@ NODEJS_VERSION=${NODEJS_VERSION:-22.23.1}
PNPM_VERSION=${PNPM_VERSION:-10.28.1}
RUSTUP_DEFAULT_TOOLCHAIN=${RUSTUP_DEFAULT_TOOLCHAIN:-stable}
RUSTUP_VERSION=${RUSTUP_VERSION:-1.29.0}
+# "hardened" images get Python from a Docker Hardened Image base, "legacy" ones compile it from
+# source on top of a plain debian-slim base.
+AIRFLOW_IMAGE_FLAVOR=${AIRFLOW_IMAGE_FLAVOR:-hardened}
+AIRFLOW_PYTHON_VERSION=${AIRFLOW_PYTHON_VERSION:-3.13.16}
COSIGN_VERSION=${COSIGN_VERSION:-3.0.5}
+if [[ "${AIRFLOW_IMAGE_FLAVOR}" == "hardened" ]]; then
+ # The Debian 12 hardened images ship Python under /opt/python, the Debian 13 ones install it as
+ # Debian packages under /usr - so ask the base image's Python where it lives.
+ PYTHON_HOME=${PYTHON_HOME:-$(python3 -c 'import sys; print(sys.base_prefix)')}
+ # Read before apt runs: a package depending on python3 can pull another Python version in.
+ BASE_PYTHON_MAJOR_MINOR=$(python3 -c 'import sys; print(f"{sys.version_info[0]}.{sys.version_info[1]}")')
+elif [[ "${AIRFLOW_IMAGE_FLAVOR}" == "legacy" ]]; then
+ PYTHON_HOME=${PYTHON_HOME:-/usr/python}
+else
+ echo
+ echo "ERROR! AIRFLOW_IMAGE_FLAVOR should be 'hardened' or 'legacy', not '${AIRFLOW_IMAGE_FLAVOR}'."
+ echo
+ exit 1
+fi
if [[ "${1}" == "runtime" ]]; then
INSTALLATION_TYPE="RUNTIME"
@@ -62,38 +78,25 @@ freetds-dev \
git \
graphviz \
graphviz-dev \
+gzip \
krb5-user \
-lcov \
ldap-utils \
-libbluetooth-dev \
-libbz2-dev \
libc6-dev \
-libdb-dev \
libev-dev \
libev4 \
libffi-dev \
-libgdbm-compat-dev \
-libgdbm-dev \
libgeos-dev \
libkrb5-dev \
libldap2-dev \
libleveldb-dev \
libleveldb1d \
-liblzma-dev \
-libncurses5-dev \
-libreadline6-dev \
libsasl2-2 \
libsasl2-dev \
libsasl2-modules \
-libsqlite3-dev \
libssl-dev \
libxmlsec1 \
libxmlsec1-dev \
-libzstd-dev \
locales \
-lsb-release \
-lzma \
-lzma-dev \
openssh-client \
openssl \
pkg-config \
@@ -102,14 +105,26 @@ sasl2-bin \
sqlite3 \
sudo \
tdsodbc \
-tk-dev \
unixodbc \
unixodbc-dev \
-uuid-dev \
wget \
xz-utils \
zlib1g-dev \
"
+ if [[ "${AIRFLOW_IMAGE_FLAVOR}" == "legacy" ]]; then
+ # Needed only to compile Python from sources - without them it builds, but silently
+ # lacks modules such as sqlite3, lzma or readline.
+ local debian_version
+ # shellcheck disable=SC1091
+ debian_version=$(. /etc/os-release; printf '%s\n' "$VERSION_CODENAME")
+ if [[ "${debian_version}" == "bookworm" ]]; then
+ DEV_APT_DEPS+="lcov libbluetooth-dev libbz2-dev libdb-dev libgdbm-compat-dev libgdbm-dev \
+liblzma-dev libncurses5-dev libreadline6-dev libsqlite3-dev libzstd-dev lzma lzma-dev tk-dev uuid-dev"
+ else
+ DEV_APT_DEPS+="lcov libbluetooth-dev libbz2-dev libdb-dev libgdbm-compat-dev libgdbm-dev \
+liblzma-dev libncurses-dev libreadline-dev libsqlite3-dev libzstd-dev tk-dev uuid-dev"
+ fi
+ fi
export DEV_APT_DEPS
fi
}
@@ -123,15 +138,29 @@ function get_runtime_apt_deps() {
echo
echo "DEBIAN CODENAME: ${debian_version}"
echo
- debian_version_apt_deps="\
+ if [[ "${debian_version}" == "bookworm" ]]; then
+ debian_version_apt_deps="\
libffi8 \
libldap-2.5-0 \
libssl3 \
netcat-openbsd\
"
+ else
+ # trixie renamed the libraries that moved to a 64-bit time_t (libssl3 -> libssl3t64) and
+ # dropped the soname from the LDAP library package name.
+ debian_version_apt_deps="\
+libffi8 \
+libldap2 \
+libssl3t64 \
+netcat-openbsd\
+"
+ fi
echo
echo "APPLIED INSTALLATION CONFIGURATION FOR DEBIAN VERSION: ${debian_version}"
echo
+ # libxmlsec1-openssl was added because libxmlsec1 ships no crypto engine of its own - the engines
+ # are separate packages - so the "xmlsec" module (pulled in by python3-saml) imported with
+ # "libxmlsec1-openssl.so.1: cannot open shared object file" without it.
if [[ "${RUNTIME_APT_DEPS=}" == "" ]]; then
RUNTIME_APT_DEPS="\
${debian_version_apt_deps} \
@@ -150,8 +179,8 @@ libgeos-dev \
libsasl2-2 \
libsasl2-modules \
libxmlsec1 \
+libxmlsec1-openssl \
locales \
-lsb-release \
openssh-client \
rsync \
sasl2-bin \
@@ -180,10 +209,82 @@ function install_docker_cli() {
apt-get install -y --no-install-recommends docker-ce-cli
}
+function keep_image_conffiles() {
+ # The hardened base images ship a modified /etc/debian_version, so any apt run that upgrades
+ # base-files stops at dpkg's interactive conffile prompt and fails a non-interactive build. Making
+ # dpkg keep the image's copy by default covers every apt run - this build's and those of images
+ # extending it - so no install command has to remember the flags.
+ mkdir -p /etc/dpkg/dpkg.cfg.d
+ printf '%s\n' force-confdef force-confold > /etc/dpkg/dpkg.cfg.d/airflow-keep-conffiles
+}
+
+function refresh_ldconfig_after_dpkg() {
+ # The hardened base images ship libc-bin without its dpkg trigger, so installing a shared library
+ # does not refresh the ldconfig cache and ctypes.util.find_library() - which reads that cache -
+ # cannot find it (for example pyenchant's "The 'enchant' C library was not found"). The hook covers
+ # every apt run - this build's and those of images extending it.
+ mkdir -p /etc/apt/apt.conf.d
+ echo 'DPkg::Post-Invoke { "if [ -x /sbin/ldconfig ]; then /sbin/ldconfig; fi"; };' \
+ > /etc/apt/apt.conf.d/99airflow-ldconfig
+}
+
+function restore_debian_base_files() {
+ # The hardened base images ship a minimal /etc, but Debian maintainer scripts assume the files
+ # a stock Debian has: sasl2-bin chowns its run directory to the "sasl" group from base-passwd,
+ # and tmux registers its shell with add-shell, which reads /etc/shells. The base-passwd package
+ # only ships the reference copies of the account files - update-passwd is what merges them
+ # into /etc.
+ # libpam-runtime generates the /etc/pam.d/common-* files that the PAM configs already in the
+ # image "@include" - without them "adduser --gecos" aborts with a PAM error from chfn.
+ # Debian Policy lets a maintainer script rely on any essential package without declaring it, and
+ # libgcrypt20 takes that up: its postinst runs a helper with a "#!/bin/dash" shebang. The hardened
+ # images ship /bin/sh as a symlink to bash and no dash at all, so configuring the package aborts
+ # with exit 127 and takes the whole apt transaction with it. Installing it first restores the
+ # assumption, and hands /bin/sh back to dash the way a stock Debian has it.
+ apt-get install -y --no-install-recommends dash
+ apt-get install -y --no-install-recommends base-passwd libpam-runtime
+ update-passwd
+ # The Debian 13 hardened images strip /etc/pam.d further: libpam-runtime and passwd are already
+ # installed there, so installing them generates nothing, and passwd, chpasswd and chfn abort with
+ # "pam_start() failed". Reinstalling passwd brings back its deleted PAM configs, and
+ # pam-auth-update - which treats the deleted common-* files as local changes - needs --force.
+ apt-get install -y --no-install-recommends --reinstall -o Dpkg::Options::=--force-confmiss passwd
+ if [[ ! -e /etc/pam.d/common-auth ]]; then
+ pam-auth-update --package --force
+ fi
+ if [[ ! -e /etc/shells ]]; then
+ printf '%s\n' "# /etc/shells: valid login shells" /bin/sh /bin/bash > /etc/shells
+ fi
+ align_perl_base_with_perl
+}
+
+function align_perl_base_with_perl() {
+ # The hardened images ship their own build of perl-base, but the hardened apt repository does not
+ # always publish the matching perl - while it still publishes packages that depend on perl, such
+ # as git. Debian's perl depends on exactly its own perl-base version, so apt refuses to install
+ # git at all. Moving perl-base to the version of the perl apt can install keeps the two in step;
+ # when the repository does publish a matching perl, the versions already agree and this does nothing.
+ local perl_version
+ local perl_base_version
+ perl_version=$(apt-cache policy perl | awk '/Candidate:/ {print $2}')
+ perl_base_version=$(dpkg-query -W -f='${Version}' perl-base 2>/dev/null || true)
+ if [[ -n "${perl_version}" && "${perl_version}" != "(none)" && -n "${perl_base_version}" \
+ && "${perl_base_version}" != "${perl_version}" ]]; then
+ apt-get install -y --no-install-recommends --allow-downgrades "perl-base=${perl_version}"
+ fi
+}
+
function install_debian_dev_dependencies() {
+ if [[ "${AIRFLOW_IMAGE_FLAVOR}" == "hardened" ]]; then
+ keep_image_conffiles
+ refresh_ldconfig_after_dpkg
+ fi
apt-get update
apt-get install -yqq --no-install-recommends apt-utils >/dev/null 2>&1
- apt-get install -y --no-install-recommends wget curl gnupg2 lsb-release ca-certificates
+ if [[ "${AIRFLOW_IMAGE_FLAVOR}" == "hardened" ]]; then
+ restore_debian_base_files
+ fi
+ apt-get install -y --no-install-recommends wget curl gnupg2 ca-certificates
# shellcheck disable=SC2086
export ${ADDITIONAL_DEV_APT_ENV?}
if [[ ${DEV_APT_COMMAND} != "" ]]; then
@@ -213,14 +314,25 @@ function install_additional_dev_dependencies() {
}
function link_python() {
+ # Airflow images have always exposed Python under /usr/python - documentation, volume mounts and
+ # user customizations refer to that path - while the hardened base images ship it in /opt/python
+ # (Debian 12) or /usr (Debian 13), so keep the historical location working as a symlink.
+ if [[ ! -e /usr/python ]]; then
+ ln -sv "${PYTHON_HOME}" /usr/python
+ fi
+ # The hardened base images have no /usr/local tree at all
+ mkdir -p /usr/local/bin /usr/local/lib
# link python binaries to /usr/local/bin and /usr/python/bin with and without 3 suffix
# Links in /usr/local/bin are needed for tools that expect python to be there
# Links in /usr/python/bin are needed for tools that are detecting home of python installation including
# lib/site-packages. The /usr/python/bin should be first in PATH in order to help with the last part.
for dst in pip3 python3 python3-config; do
src="$(echo "${dst}" | tr -d 3)"
+ if [[ ! -e "/usr/python/bin/${dst}" ]]; then
+ continue
+ fi
echo "Linking ${dst} in /usr/local/bin and /usr/python/bin"
- ln -sv "/usr/python/bin/${dst}" "/usr/local/bin/${dst}"
+ ln -sfv "/usr/python/bin/${dst}" "/usr/local/bin/${dst}"
for dir in /usr/local/bin /usr/python/bin; do
if [[ ! -e "${dir}/${src}" ]]; then
echo "Creating ${src} - > ${dst} link in ${dir}"
@@ -228,37 +340,103 @@ function link_python() {
fi
done
done
- for dst in /usr/python/lib/*
- do
- src="/usr/local/lib/$(basename "${dst}")"
- if [[ -e "${src}" ]]; then
- rm -rf "${src}"
- fi
- echo "Linking ${dst} to ${src}"
- ln -sv "${dst}" "${src}"
- done
+ # A Python installed under /usr already has its libraries where the dynamic linker looks, and
+ # linking all of /usr/lib into /usr/local/lib would only shadow the system libraries.
+ if [[ "$(readlink -f "${PYTHON_HOME}")" != "/usr" ]]; then
+ for dst in /usr/python/lib/*
+ do
+ src="/usr/local/lib/$(basename "${dst}")"
+ if [[ -e "${src}" ]]; then
+ rm -rf "${src}"
+ fi
+ echo "Linking ${dst} to ${src}"
+ ln -sv "${dst}" "${src}"
+ done
+ fi
ldconfig
}
-function install_debian_runtime_dependencies() {
- apt-get update
- apt-get install --no-install-recommends -yqq apt-utils >/dev/null 2>&1
- apt-get install -y --no-install-recommends wget curl gnupg2 lsb-release ca-certificates
- # shellcheck disable=SC2086
- export ${ADDITIONAL_RUNTIME_APT_ENV?}
- if [[ "${RUNTIME_APT_COMMAND}" != "" ]]; then
- bash -o pipefail -o errexit -o nounset -o nolog -c "${RUNTIME_APT_COMMAND}"
+function restore_thread_stack_size() {
+ # The hardened base images build Python with -DTHREAD_STACK_SIZE=0x100000, so every thread Python
+ # starts gets a 1 MiB stack instead of glibc's default (the 8 MiB "ulimit -s" the previous images
+ # used). On Python 3.12 and 3.13 the C recursion guard is a fixed depth count sized for the larger
+ # stack, so deeply nested input - e.g. a JSON body parsed in a web server worker thread - overflows
+ # the stack and kills the process instead of raising RecursionError. A .pth file runs at every
+ # interpreter start-up without taking the sitecustomize module name users may already rely on.
+ local site_packages
+ site_packages="$(/usr/python/bin/python -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')"
+ echo "Restoring the 8 MiB default thread stack size in ${site_packages}"
+ echo "import threading; threading.stack_size(8 * 1024 * 1024)" > "${site_packages}/airflow-thread-stack-size.pth"
+}
+
+function compile_python_stdlib() {
+ # The hardened base images ship the standard library with no .pyc files at all. Python then misses
+ # the bytecode cache on every stdlib import, and in a read-only or non-writable directory it cannot
+ # create one - each miss leaves a negative dentry in the kernel, which grows without bound in a
+ # long-running container and can exhaust memory on the host. Compiling the standard library here
+ # restores what the previously compiled-in-image Python shipped.
+ # See https://github.com/apache/airflow/pull/58944 and https://lwn.net/Articles/814535/
+ local stdlib
+ stdlib="$(/usr/python/bin/python -c 'import sysconfig; print(sysconfig.get_paths()["stdlib"])')"
+ echo "Compiling Python standard library in ${stdlib}"
+ # compileall exits non-zero when any file fails to compile, and the standard library ships files
+ # that are meant not to compile (deliberately broken syntax used by the test suite).
+ /usr/python/bin/python -m compileall -q -j "$(nproc)" -o 0 -o 1 -o 2 "${stdlib}" || true
+}
+
+function hold_hardened_python() {
+ # The Debian 13 hardened images install their Python as packages that conflict with Debian's own
+ # python3. A package depending on python3 - for example NodeSource's nodejs - therefore makes apt
+ # remove the image's Python and install Debian's, without asking. Holding the packages turns that
+ # into an apt error instead, in this build and in images extending it.
+ local packages
+ packages=$(dpkg-query -W -f='${db:Status-Abbrev} ${Package}\n' 'python-3.*' 'libpython-3.*' 2>/dev/null \
+ | awk '$1 == "ii" {print $2}' || true)
+ if [[ -n "${packages}" ]]; then
+ # shellcheck disable=SC2086
+ apt-mark hold ${packages}
fi
- if [[ "${ADDITIONAL_RUNTIME_APT_COMMAND}" != "" ]]; then
- bash -o pipefail -o errexit -o nounset -o nolog -c "${ADDITIONAL_RUNTIME_APT_COMMAND}"
+}
+
+function check_no_system_python() {
+ # Python - from the hardened base image or compiled from sources - must stay the only Python in the
+ # image. A system Python pulled in as a dependency of an apt package shares its shared libraries
+ # with ours and leads to errors such as:
+ # /usr/python/lib/python3.11/lib-dynload/_ssl.cpython-311-aarch64-linux-gnu.so: undefined symbol: _PyModule_Add
+ # Debian names its Python libraries "libpython3.13". The trixie hardened images register their own
+ # Python with dpkg as "libpython-3.13", which is the Python we want, so it must not match.
+ # Debian 13 hardened images package their Python as "python-3.13" and serve other versions from
+ # their apt repository, so a package depending on python3 can also pull in a hardened Python of a
+ # different version than the image's own.
+ local other_hardened_python=""
+ if [[ -n "${BASE_PYTHON_MAJOR_MINOR=}" ]]; then
+ other_hardened_python=$(dpkg -l | awk '/^ii (lib)?python-3\.[0-9]+/ {print $2}' \
+ | grep -v -E "python-${BASE_PYTHON_MAJOR_MINOR//./\\.}(-|:|$)" || true)
+ fi
+ if [[ -n "${other_hardened_python}" ]]; then
+ echo
+ echo "ERROR! A Python other than the image's ${BASE_PYTHON_MAJOR_MINOR} was installed: ${other_hardened_python//$'\n'/ }"
+ echo
+ apt-get install -yqq aptitude >/dev/null
+ aptitude why "$(echo "${other_hardened_python}" | head -1 | cut -d: -f1)"
+ echo
+ exit 1
+ fi
+ if dpkg -l | grep -E '^ii libpython3\.[0-9]+' >/dev/null; then
+ echo
+ echo "ERROR! System python is installed by one of the previous steps"
+ echo
+ echo "Please make sure that no python packages are installed by default. Displaying the reason why libpython is installed:"
+ echo
+ apt-get install -yqq aptitude >/dev/null
+ aptitude why "$(dpkg -l | grep -E '^ii libpython3\.[0-9]+' | head -1 | awk '{print $2}')"
+ echo
+ exit 1
+ else
+ echo
+ echo "GOOD! System python is not installed - OK"
+ echo
fi
- apt-get update
- # shellcheck disable=SC2086
- apt-get install -y --no-install-recommends ${RUNTIME_APT_DEPS} ${ADDITIONAL_RUNTIME_APT_DEPS}
- apt-get autoremove -yqq --purge
- apt-get clean
- link_python
- rm -rf /var/lib/apt/lists/* /var/log/*
}
function install_cosign() {
@@ -282,27 +460,7 @@ function install_cosign() {
}
function install_python() {
- # If system python (3.11 in bookworm) is installed (via automatic installation of some dependencies for example), we need
- # to fail and make sure that it is not there, because there can be strange interactions if we install
- # newer version and system libraries are installed, because
- # when you create a virtualenv part of the shared libraries of Python can be taken from the system
- # Installation leading to weird errors when you want to install some modules - for example when you install ssl:
- # /usr/python/lib/python3.11/lib-dynload/_ssl.cpython-311-aarch64-linux-gnu.so: undefined symbol: _PyModule_Add
- if dpkg -l | grep '^ii' | grep '^ii libpython' >/dev/null; then
- echo
- echo "ERROR! System python is installed by one of the previous steps"
- echo
- echo "Please make sure that no python packages are installed by default. Displaying the reason why libpython3.11 is installed:"
- echo
- apt-get install -yqq aptitude >/dev/null
- aptitude why libpython3.11
- echo
- exit 1
- else
- echo
- echo "GOOD! System python is not installed - OK"
- echo
- fi
+ # Only the legacy image flavor compiles Python - the hardened flavor gets it from the base image.
wget --tries=3 --waitretry=5 -O python.tar.xz "https://www.python.org/ftp/python/${AIRFLOW_PYTHON_VERSION%%[a-z]*}/Python-${AIRFLOW_PYTHON_VERSION}.tar.xz"
local major_minor_version
major_minor_version="${AIRFLOW_PYTHON_VERSION%.*}"
@@ -348,21 +506,13 @@ function install_python() {
EXTRA_CFLAGS="${EXTRA_CFLAGS:-} -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer";
LDFLAGS="$(dpkg-buildflags --get LDFLAGS)"
LDFLAGS="${LDFLAGS:--Wl},--strip-all"
- # Link-Time Optimization (LTO) uses MD5 checksums for object file verification during
- # compilation. In FIPS mode, MD5 is blocked as a non-approved algorithm, causing builds
- # to fail. The PYTHON_LTO variable allows disabling LTO for FIPS-compliant builds.
- # See: https://github.com/apache/airflow/issues/58337
- local lto_option=""
- if [[ "${PYTHON_LTO:-true}" == "true" ]]; then
- lto_option="--with-lto"
- fi
local build_log
build_log=$(mktemp)
echo "Building Python ${AIRFLOW_PYTHON_VERSION} from source..."
if ! (
./configure --enable-optimizations --prefix=/usr/python/ --with-ensurepip --build="$gnuArch" \
--enable-loadable-sqlite-extensions --enable-option-checking=fatal \
- --enable-shared ${lto_option} && \
+ --enable-shared --with-lto && \
make -s -j "$(nproc)" "EXTRA_CFLAGS=${EXTRA_CFLAGS:-}" \
"LDFLAGS=${LDFLAGS:--Wl},-rpath='\$\$ORIGIN/../lib'" python && \
make -s -j "$(nproc)" install
@@ -375,6 +525,9 @@ function install_python() {
exit 1
fi
rm -f "${build_log}"
+ # CPython builds without the optional modules whose -dev packages are missing, so check them here
+ # rather than finding out from a broken image.
+ /usr/python/bin/python3 -c "import bz2, ctypes, dbm.gnu, lzma, readline, sqlite3, ssl, uuid, zlib"
cd /
rm -rf /usr/src/python
find /usr/python -depth \
@@ -385,6 +538,40 @@ function install_python() {
link_python
}
+function install_debian_runtime_dependencies() {
+ if [[ "${AIRFLOW_IMAGE_FLAVOR}" == "hardened" ]]; then
+ keep_image_conffiles
+ refresh_ldconfig_after_dpkg
+ fi
+ apt-get update
+ apt-get install --no-install-recommends -yqq apt-utils >/dev/null 2>&1
+ if [[ "${AIRFLOW_IMAGE_FLAVOR}" == "hardened" ]]; then
+ restore_debian_base_files
+ fi
+ apt-get install -y --no-install-recommends wget curl gnupg2 ca-certificates
+ # shellcheck disable=SC2086
+ export ${ADDITIONAL_RUNTIME_APT_ENV?}
+ if [[ "${RUNTIME_APT_COMMAND}" != "" ]]; then
+ bash -o pipefail -o errexit -o nounset -o nolog -c "${RUNTIME_APT_COMMAND}"
+ fi
+ if [[ "${ADDITIONAL_RUNTIME_APT_COMMAND}" != "" ]]; then
+ bash -o pipefail -o errexit -o nounset -o nolog -c "${ADDITIONAL_RUNTIME_APT_COMMAND}"
+ fi
+ apt-get update
+ # shellcheck disable=SC2086
+ apt-get install -y --no-install-recommends ${RUNTIME_APT_DEPS} ${ADDITIONAL_RUNTIME_APT_DEPS}
+ apt-get autoremove -yqq --purge
+ apt-get clean
+ check_no_system_python
+ link_python
+ if [[ "${AIRFLOW_IMAGE_FLAVOR}" == "hardened" ]]; then
+ hold_hardened_python
+ restore_thread_stack_size
+ compile_python_stdlib
+ fi
+ rm -rf /var/lib/apt/lists/* /var/log/*
+}
+
function install_golang() {
curl --retry 3 --retry-delay 5 "https://dl.google.com/go/go${GOLANG_MAJOR_MINOR_VERSION}.linux-$(dpkg --print-architecture).tar.gz" -o "go${GOLANG_MAJOR_MINOR_VERSION}.linux.tar.gz"
rm -rf /usr/local/go && tar -C /usr/local -xzf go"${GOLANG_MAJOR_MINOR_VERSION}".linux.tar.gz
@@ -481,7 +668,15 @@ if [[ "${INSTALLATION_TYPE}" == "RUNTIME" ]]; then
else
get_dev_apt_deps
install_debian_dev_dependencies
- install_python
+ check_no_system_python
+ if [[ "${AIRFLOW_IMAGE_FLAVOR}" == "hardened" ]]; then
+ link_python
+ hold_hardened_python
+ restore_thread_stack_size
+ compile_python_stdlib
+ else
+ install_python
+ fi
install_additional_dev_dependencies
install_rustup
if [[ "${INSTALLATION_TYPE}" == "CI" ]]; then
diff --git a/scripts/docker/install_postgres.sh b/scripts/docker/install_postgres.sh
index 4af446a7a68..9a6c625bdee 100644
--- a/scripts/docker/install_postgres.sh
+++ b/scripts/docker/install_postgres.sh
@@ -43,7 +43,7 @@ install_postgres_client() {
common::import_trusted_gpg "7FCC7D46ACCC4CF8" "postgres"
- echo "deb [arch=amd64,arm64] https://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > \
+ echo "deb [arch=amd64,arm64] https://apt.postgresql.org/pub/repos/apt/ $(common::debian_codename)-pgdg main" > \
/etc/apt/sources.list.d/pgdg.list
apt-get update
apt-get install --no-install-recommends -y "${packages[@]}"
diff --git a/scripts/docker/keys/microsoft-2025.asc b/scripts/docker/keys/microsoft-2025.asc
new file mode 100644
index 00000000000..d6381a08150
--- /dev/null
+++ b/scripts/docker/keys/microsoft-2025.asc
@@ -0,0 +1,29 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQINBGVUhiwBEADF3TWX0HMi2+BdQfJrSdQkZTE4qk4vV2ooAMn8vWA2DGI88JOl
+k1LwhZGEqJv5TsKTyNEMWb3NXhR1ZZ5uQPvf6iN0806cq83s096F85GUtjzfGLQj
+Zo3FhDSKeHz3mhthQ4QP4bwYUmSpWs6e+/ZSFYYc3yU8mInDM4SNzrqr4x2ltmf+
+3RWkoYYo1SpG521A9+1zi7xzz6IHpAk6MdIcTj7mHxXd6ovmXkvHUhKbXGkybHPn
+iupWokDaJZgV4+q6kc7zVgTVnwmXV7NHQhWSyOm/BmYVcpmrkCSgSH18SArFjR6Q
+KyJ9VuUo1mJEUGnEakQSaOn1UAYtO8Mh4cXXD4833G0BLjiFNOL0XRUNh35pKvcT
+my/HnXvRXtpzAzTtANPxIbjli/veagU+JRWhtjtfONz0wQ5Bv1zFjnM9ewxFNPPo
+7Jp9WCVeUKFZcZJo8r/k7Y4d0Y1WINOPniSCNhKcD0pva3gXLcxfdnZjdMSj++ba
+XlAstjw0Oyty0EXoHXCMpelMoa+DQ7KSDGKrOtm5YFAP6Ki4go1Tt2q8nmul36cZ
+Zot6eoPG/qKxW+dvmSrWhQCcfd74VbhECbzXiCFLHadq85C1K5rrLM6oVr1u7K6O
+jlc1aitGgZECi6fvu61QhpUvHjCegRWzMIhah9qrv4lvxFFcA+a1jwXlnwARAQAB
+tEJNaWNyb3NvZnQgQ29ycG9yYXRpb24gLSBHZW5lcmFsIEdQRyBTaWduZXIgPGdw
+Z3NpZ25AbWljcm9zb2Z0LmNvbT6JAjgEEwEIACIFAmVUhiwCGwMGCwkIBwMCBhUI
+AgkKCwQWAgMBAh4BAheAAAoJEO5Nd5L3SBgrDc0P/0Ubx0vqD/DgyhiP0bIs8euO
+iA5BQvOCiroIkhSkFbAw8rT9a/XtRTRM2l4I8c2M1ZX9i/0wWihmFUJhiVHyRxkl
+ZcEFv+ieBuhvD1gPOVLZg3To8yOTrcOnHe+FuKqA6u+3xBn2AmAWeck9o0NKhtnm
+5ckweos+Qj9NoxaZX8UeGFstOiTBJeyhuJjthQ+3M0BvTxEaRcLXGSXSGSgZ00ii
+YSLNgOMPF+C22bXBL/erClEYkIGCctqPvyrhV/GVNnGk2ALyJqdK+BaJeGh9mBJa
+ZrP3l6vFxsAI0RNCNU1s5QaFzfFzFkiUnG/aoyuwh4xmsB+uyVkR+KigPK9gfF3S
+nU7AqcdhSbUA6A0DGDRkHauHM5Wtc7730LdjiNDXbYwG/yXmDYNasoszmItZzh77
+HiQxYA5dNB9r9QJS2rHV/qe+heAJ5Rub5kxcu33DGL30qG7Q9+HRTu0oSEOIUFyT
+aOJJnNUiB2D4hoKKnr5U8FYOZ7KvDcG7cDvInqYtGpNfrnIf94VeB9WJY6DbDQSA
+F5yHb6X8FS0x3lMT2H1l6RRyr0278kyO18VBudtlnonC+Y1UT7eqAk6WjS5CitPX
+T3Hc7jCURugXrc51igKa+p67yAaybEIuVyWF6JaINKRqiUqEPVXnHELXPbBmiHW5
+1HwdbKTMzgF8bu1JI+tQ
+=lIzW
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/scripts/in_container/run_ci_tests.sh b/scripts/in_container/run_ci_tests.sh
index 9bb1d149cf5..77f5e69abb7 100755
--- a/scripts/in_container/run_ci_tests.sh
+++ b/scripts/in_container/run_ci_tests.sh
@@ -23,17 +23,22 @@ echo "Starting the tests with those pytest arguments:" "${@}"
echo
set +e
+# A segfault kills the process before buffered output reaches the log, which leaves nothing
+# to diagnose from; both of these make the crash describe itself instead.
+export PYTHONFAULTHANDLER="1"
+export PYTHONUNBUFFERED="1"
+
pytest "${@}"
RES=$?
if [[ ${RES} == "139" ]]; then
echo "${COLOR_YELLOW}Sometimes Pytest fails at exiting with segfault, but all tests actually passed${COLOR_RESET}"
- echo "${COLOR_YELLOW}We should ignore such case. Checking if junitxml file ${RESULT_LOG_FILE} is there with 0 errors and failures${COLOR_RESET}"
- if [[ -f ${RESULT_LOG_FILE} ]]; then
+ echo "${COLOR_YELLOW}We should ignore such case. Checking if junitxml file ${RESULT_LOG_FILE:-<unset>} is there with 0 errors and failures${COLOR_RESET}"
+ if [[ -n ${RESULT_LOG_FILE:-} && -f ${RESULT_LOG_FILE} ]]; then
python "${AIRFLOW_SOURCES}/scripts/in_container/check_junitxml_result.py" "${RESULT_LOG_FILE}"
RES=$?
else
- echo "${COLOR_YELLOW}JunitXML file ${RESULT_LOG_FILE} does not exist. Proceeding with failure as we cannot check if there were no failures.${COLOR_RESET}"
+ echo "${COLOR_YELLOW}JunitXML file ${RESULT_LOG_FILE:-<unset>} does not exist. Proceeding with failure as we cannot check if there were no failures.${COLOR_RESET}"
fi
fi
diff --git a/scripts/tests/ci/prek/test_upgrade_important_versions.py b/scripts/tests/ci/prek/test_upgrade_important_versions.py
index 6eb9d28d663..7a765e55ab8 100644
--- a/scripts/tests/ci/prek/test_upgrade_important_versions.py
+++ b/scripts/tests/ci/prek/test_upgrade_important_versions.py
@@ -237,3 +237,65 @@ def test_flit_core_upgrade_updates_provider_pyproject_template(monkeypatch):
)
assert 'requires = ["flit_core==99.0.0"]' in new_content
+
+
+def _catalog_responses(uiv, definitions: dict[str, str]):
+ def get(url, **kwargs):
+ response = mock.MagicMock(spec=uiv.requests.Response)
+ response.text = next(text for debian, text in definitions.items() if f"/debian-{debian}/" in url)
+ return response
+
+ return mock.create_autospec(uiv.requests.get, side_effect=get)
+
+
+@pytest.mark.parametrize(
+ ("definitions", "expected"),
+ [
+ pytest.param(
+ {
+ "13": "tags:\n - 3.14-debian13-dev\n - 3.14.8-debian13-dev\nplatforms:\n",
+ "12": "tags:\n - 3.14-debian12-dev\n - 3.14.8-debian12-dev\nplatforms:\n",
+ },
+ "3.14.8",
+ id="floating-and-patchlevel-tags",
+ ),
+ pytest.param(
+ {
+ "13": "tags:\n - 3.14.9-debian13-dev\n - 3.14.15-debian13-dev\n",
+ "12": "tags:\n - 3.14.9-debian12-dev\n - 3.14.15-debian12-dev\n",
+ },
+ "3.14.15",
+ id="highest-patchlevel-wins-numerically",
+ ),
+ pytest.param(
+ {
+ "13": "tags:\n - 3.14.9-debian13-dev\n",
+ "12": "tags:\n - 3.14.8-debian12-dev\n",
+ },
+ "3.14.8",
+ id="patchlevel-published-for-every-debian-release",
+ ),
+ ],
+)
+def test_python_patchlevel_comes_from_the_hardened_image_catalog(monkeypatch, definitions, expected):
+ from ci.prek import upgrade_important_versions as uiv
+
+ get = _catalog_responses(uiv, definitions)
+ monkeypatch.setattr(uiv.requests, "get", get)
+
+ assert uiv.get_latest_python_version("3.14") == expected
+ assert [call.args[0].split("/image/python/")[1] for call in get.call_args_list] == [
+ "debian-13/3.14-dev.yaml",
+ "debian-12/3.14-dev.yaml",
+ ]
+
+
+def test_python_patchlevel_exits_when_the_catalog_lists_no_tag(monkeypatch):
+ from ci.prek import upgrade_important_versions as uiv
+
+ response = mock.MagicMock(spec=uiv.requests.Response)
+ response.text = "tags:\n - 3.14-debian12-dev\n"
+ monkeypatch.setattr(uiv.requests, "get", mock.create_autospec(uiv.requests.get, return_value=response))
+
+ with pytest.raises(SystemExit):
+ uiv.get_latest_python_version("3.14")