ARG BASE_IMAGE=python:3.12-slim-trixie
FROM ${BASE_IMAGE}

# gRPC ships a separate CA bundle that can lag behind OS distrust updates.
ENV GRPC_DEFAULT_SSL_ROOTS_FILE_PATH=/etc/ssl/certs/ca-certificates.crt

# The -slim base ships without curl/gnupg/ca-certificates, so bootstrap them
# before configuring third-party apt repositories.
RUN apt-get -qq update \
    && apt-get -qq install -y --no-install-recommends \
         curl ca-certificates gnupg \
    && rm -rf /var/lib/apt/lists/*
RUN --mount=type=bind,source=ingestion/scripts/patch_python_seeds.py,target=/tmp/patch_python_seeds.py \
    python -m pip download --no-cache-dir --no-deps --dest /tmp/python-pip "pip @ https://files.pythonhosted.org/packages/3f/06/3e4420302363fbb71be3c7f35ce042f54bfca330e3e66b3a2e3c4cca579f/pip-26.2.2-py3-none-any.whl#sha256=fd7f4dfa8190e014013b9f08f3e973f4e4c26ffd179a204895533a7254813a70" \
    && python -m pip install --no-cache-dir --upgrade /tmp/python-pip/*.whl \
    && python -m pip install --no-cache-dir --upgrade "certifi>=2026.10.19" \
    && python /tmp/patch_python_seeds.py /tmp/python-pip/pip-26.2.2-py3-none-any.whl \
    && rm -rf /tmp/python-pip
# apt-key was removed in Debian 13. The repo was also pointing at Debian 11
# while the image was Debian 12 -- now matched to the actual base. The
# debian/13 "prod" repo is signed with Microsoft's newer General GPG Signer
# key (EE4D7792F748182B), which is NOT included in keys/microsoft.asc (that
# file only has the older Release-signing key) -- so the key and matching
# sources.list entry are taken from Microsoft's own config package instead of
# hand-dearmoring the wrong key.
RUN curl -fsSL -o /tmp/packages-microsoft-prod.deb \
      https://packages.microsoft.com/config/debian/13/packages-microsoft-prod.deb \
  && dpkg -i /tmp/packages-microsoft-prod.deb \
  && rm -f /tmp/packages-microsoft-prod.deb

# Install Dependencies (listed in alphabetical order)
RUN dpkg --configure -a \
    && apt-get -qq update \
    && apt-get -qq install -y \
    build-essential \
    default-libmysqlclient-dev \
    freetds-dev \
    gcc \
    gnupg \
    # Renamed by Debian's 64-bit time_t transition; libaio1 no longer exists.
    libaio1t64 \
    libevent-dev \
    libffi-dev \
    libpq-dev \
    librdkafka-dev \
    libsasl2-dev \
    libsasl2-2 \
    libsasl2-modules \
    libsasl2-modules-gssapi-mit \
    libssl-dev \
    libxml2 \
    libkrb5-dev \
    default-jre-headless \
    openssl \
    # mysqlclient 2.2+ locates libmysqlclient through pkg-config (2.1.x used
    # mysql_config) and has no Linux wheels, so it always builds from source.
    # The slim base omits pkg-config, unlike the python:3.12-bookworm base.
    pkg-config \
    # unixodbc/odbcinst were pinned to bookworm's 2.3.11-2+deb12u1 to keep the
    # two in lockstep; trixie ships 2.3.12-2 for both, so the pins are dropped
    # rather than retargeted.
    odbcinst \
    tdsodbc \
    unixodbc \
    unixodbc-dev \
    unzip \
    git \
    wget --no-install-recommends \
    # Accept MSSQL ODBC License
    && ACCEPT_EULA=Y apt-get -qq install -y msodbcsql18 \
    # Pull any security updates newer than the base image for packages we or our
    # dependencies pull in. Version-specific Perl module names were removed:
    # trixie ships Perl 5.40, so perl-modules-5.36/libperl5.36 do not exist.
    # --only-upgrade is a no-op for packages not present in the image.
    && apt-get -qq install -y --only-upgrade \
         libgnutls30 libcap2 libcap2-bin openssh-client rsync sudo \
         libpam0g libpam-modules libpam-modules-bin libpam-runtime \
         curl libcurl4 libcurl3-gnutls wget \
         perl perl-base libnss3 \
    && rm -rf /var/lib/apt/lists/*

# Add updated postgres/redshift dependencies based on libq
ENV DEBIAN_FRONTEND=noninteractive
RUN curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \
      | gpg --dearmor -o /usr/share/keyrings/postgresql.gpg
RUN echo "deb [signed-by=/usr/share/keyrings/postgresql.gpg] https://apt.postgresql.org/pub/repos/apt/ trixie-pgdg main" > /etc/apt/sources.list.d/pgdg.list; \
    apt-get -qq update; \
    apt-get -qq install --no-install-recommends -y libpq-dev; \
    apt-get -qq autoremove -yqq --purge; \
    apt-get -qq clean && rm -rf /var/lib/apt/lists/*

# Oracle Instant Client 19.x is linked against libaio.so.1, but Debian 13's
# 64-bit time_t transition ships that library as libaio.so.1t64 (package
# libaio1t64). Installing the package satisfies apt but not the linker, so
# python-oracledb thick mode fails at runtime with DPI-1047. Provide the legacy
# soname. The
# path is derived rather than hard-coded because the multiarch directory
# differs between amd64 and arm64 builds.
RUN set -eu; \
    lib="$(find /usr/lib -name 'libaio.so.1t64' -print -quit)"; \
    test -n "$lib"; \
    ln -sf "$lib" "$(dirname "$lib")/libaio.so.1"

# `||` inside a single [ ] is invalid test syntax: it made this check always
# fall through to the x86_64 branch, so arm64 builds installed the amd64 Oracle
# client. Evaluate uname once and combine with a shell-level ||.
RUN arch="$(uname -m)"; \
    if [ "$arch" = "arm64" ] || [ "$arch" = "aarch64" ]; \
 then \
 wget -q https://download.oracle.com/otn_software/linux/instantclient/191000/instantclient-basic-linux.arm64-19.10.0.0.0dbru.zip -O /oracle-instantclient.zip && \
 unzip -qq -d /instantclient -j /oracle-instantclient.zip && rm -f /oracle-instantclient.zip; \
 else \
 wget -q https://download.oracle.com/otn_software/linux/instantclient/1917000/instantclient-basic-linux.x64-19.17.0.0.0dbru.zip -O /oracle-instantclient.zip && \
 unzip -qq -d /instantclient -j /oracle-instantclient.zip && rm -f /oracle-instantclient.zip; \
 fi

ENV LD_LIBRARY_PATH=/instantclient

# Install DB2 iAccess driver
# Mirrored on cdn.getcollate.io to decouple builds from IBM's CDN availability.
# Use dpkg --force-depends because the package declares old Debian package names (libodbc1, odbcinst1debian2)
# that don't exist in Debian 12, but the actual dependencies (unixodbc, odbcinst) are already installed.
# SHA256 pinned to v1.1.0.29 — matches the version production ingestion-slim images run.
RUN if [ $(uname -m) = "x86_64" ]; then \
  wget -q https://cdn.getcollate.io/deps/ingestion/ibm/ibm-iaccess-1.1.0.29-1.0.amd64.deb \
    -O /tmp/ibm-iaccess.deb && \
  echo "e60e968d2cee96b2851964456f5b31ab990b1aa47d8f2399607809f7d4514f58  /tmp/ibm-iaccess.deb" | sha256sum -c - && \
  dpkg -i --force-depends /tmp/ibm-iaccess.deb && \
  apt-get install -f -y --no-install-recommends && \
  rm -f /tmp/ibm-iaccess.deb; \
fi

WORKDIR ingestion/

# Required for Airflow DockerOperator, as we need to run the workflows from a `python main.py` command in the container.
COPY ingestion/operators/docker/*.py .

# Create a non-root user with a writable home directory.
# When the container runs with securityContext runAsUser: 1000 but no
# /etc/passwd entry for that UID, Python disables user site-packages.
# This causes runtime failures when tools like spacy download models
# via pip --user, as the installed packages are invisible to the import
# system. Creating the user ensures Python recognises UID 1000 and
# enables the standard ~/.local install path.
RUN groupadd -g 1000 openmetadata && useradd -m -u 1000 -g 1000 openmetadata
RUN chown -R openmetadata:openmetadata /ingestion
ENV HOME=/home/openmetadata
ENV PATH="/home/openmetadata/.local/bin:${PATH}"

# Upgrade interpreter-level build tools before dropping privileges so scanners do not retain
# the vulnerable base copies (pip CVE-2025-8869/3219/6357/8643; setuptools CVE-2026-59890).
RUN pip install --no-cache-dir --upgrade "setuptools>=83"

USER openmetadata

# Disable pip cache dir
# https://pip.pypa.io/en/stable/topics/caching/#avoiding-caching
ENV PIP_NO_CACHE_DIR=1
# Make pip silent
ENV PIP_QUIET=1

# GCC 14 (trixie's default) turns implicit-function-declaration,
# incompatible-pointer-types, and int-conversion from warnings into hard
# errors by default. Some pinned C-extension deps (e.g. asammdf 7.4.5) predate
# that default and fail to compile without this; it does not relax any check
# performed on our own code.
ENV CFLAGS="-Wno-error=incompatible-pointer-types -Wno-error=implicit-function-declaration -Wno-error=int-conversion"

ARG INGESTION_DEPENDENCY="all"
ARG RI_VERSION="2.0.0.0.dev0"
RUN pip install "openmetadata-ingestion[airflow]~=${RI_VERSION}"
RUN pip install "openmetadata-ingestion[${INGESTION_DEPENDENCY}]~=${RI_VERSION}"


# Temporary workaround for https://github.com/open-metadata/OpenMetadata/issues/9593
RUN [ $(uname -m) = "x86_64" ] \
  && pip install "openmetadata-ingestion[db2]~=${RI_VERSION}" \
  || echo "DB2 not supported on ARM architectures."

# Uninstalling psycopg2-binary and installing psycopg2 instead
# because the psycopg2-binary generates a architecture specific error
# while authenticating connection with the airflow, psycopg2 solves this error
RUN pip uninstall psycopg2-binary -y
RUN pip install psycopg2
# pydoris declares mysqlclient, but the Doris connector runs pydoris on PyMySQL
# (metadata/ingestion/source/database/doris/connection.py), so nothing imports the
# C extension. Removing it lets the root layer below purge the MariaDB client libraries.
RUN pip uninstall -y mysqlclient

# Drop build-only packages that leaked into the runtime image. The MariaDB client
# packages were only needed to compile mysqlclient, which was uninstalled above.
# Debian reports every MariaDB server CVE against libmariadb3 and mariadb-common,
# so keeping them would carry server CVEs into an image that has no server.
# libunbound8 arrived as a transitive build dependency and nothing here resolves
# DNS through it. apt-get needs root, so switch back briefly -- USER openmetadata
# above has no sudo access -- then restore the non-root user for the runtime image.
# The build fails if any src:mariadb binary survives the purge.
# libxml2, the Cyrus SASL plugins and librdkafka go for the same reason: Debian
# files CVEs against them with no trixie fix, and nothing here loads them. lxml
# bundles its own libxml2 (shared-mime-info is its only Debian dependent), Hive and
# Impala authenticate through pure-sasl + kerberos rather than Cyrus SASL, and the
# confluent_kafka wheel bundles its own librdkafka. libsasl2-2 and its sasldb
# plugin stay because libpq5 needs them through libldap2; the DIGEST-MD5 plugin
# those CVEs are in lives in libsasl2-modules. dpkg --purge rather than apt-get:
# apt-get would silently remove anything that still depends on these, dpkg refuses.
#
# The OS security upgrades ride along in the same root layer. trixie's base ships
# util-linux 2.41-5, which carries CVE-2025-14104, CVE-2026-13595 and
# CVE-2026-27456; trixie-security has 2.41.5-0+deb13u1.
# expat is the same story one release later. It is not in the trixie base at all
# -- it arrives transitively from the top-of-file apt layer -- and that layer's
# frozen index is exactly why the image keeps shipping 2.8.2 and being reported
# for CVE-2026-72522 while trixie-security has 2.8.3-1~deb13u1.
# openssl follows the same cache boundary. The early apt layer can retain
# 3.5.6-1~deb13u2, while trixie-security carries the CVE-2026-14457 fix in
# 3.5.7-1~deb13u2. Querying the source package upgrades openssl, libssl3t64,
# and libssl-dev together so no scanner-visible binary is left behind.
# linux-libc-dev is the third, and the reason CVE-2026-68082 is reported against
# this image: libc6-dev pulls the kernel headers in for build-essential, a
# container runs the host kernel so they are a build artifact whose kernel CVEs
# are not reachable at runtime, but scanners flag the installed version anyway.
# trixie-security carries the fixed 6.12.105-1 (DSA-6466-1), so this clears the
# finding on the next build rather than being a speculative no-op. Purging it
# instead would cascade through libc6-dev, which the -dev packages installed at
# the top of this file still depend on.
# The package sets are computed from dpkg rather than hand-listed. One source
# package produces many binaries -- here util-linux, bsdutils, login, mount,
# liblastlog2-2, libblkid1, libmount1, libsmartcols1 and libuuid1 -- and scanners
# report each separately, so a hand-written list silently leaves behind whichever
# binary it forgot, and liblastlog2-2 is exactly the one that is easy to forget.
# Asking dpkg which installed packages came from the source cannot miss one, and
# stays correct if Debian splits the source differently later.
# util-linux and openssl are asserted non-empty; expat and linux-libc-dev
# deliberately are not. util-linux and openssl are explicitly installed, so an
# empty query there means dpkg-query misbehaved and the build must not continue,
# whereas the other two are transitive and an empty query is a legitimate image
# with nothing to patch. Asserting the required packages is what the
# check is for -- `apt-get install --only-upgrade` with no package arguments
# exits 0, so an all-empty query would give a green build that shipped the
# vulnerable packages anyway. Fail closed.
# This deliberately does NOT go in the top-of-file apt RUN: that layer's cache
# key never changes, so an upgrade placed there freezes its Debian index with it
# and the image keeps shipping whatever was current when the layer was first
# built. Below the COPY and pip layers, the index is re-read on every build.
#
# The C/C++ toolchain is build-only and goes in this layer, the first point after
# the last pip install that compiles: psycopg2 above; mysqlclient (pulled in by pydoris),
# kerberos and mysql-connector-python through the extras. Scanners attribute src:gcc-NN CVEs to
# every installed binary of that source, and most of those binaries are the
# compilers and their sanitizer libraries. Nothing compiles at runtime: the DB2
# connector switches clidriver versions by swapping the driver files, not by
# rebuilding ibm_db (see ingestion/src/metadata/ingestion/source/database/db2/utils.py).
# Only build-essential and gcc are purged by name. autoremove takes what they pulled
# in, so the gcc major version is never hand-listed and nothing another package
# still needs is removed. What stays: libstdc++6, libgcc-s1 and gcc-NN-base, which
# libc6, apt, msodbcsql18, the JRE and most native wheels need; libgomp1, which
# backs numba's OpenMP threading layer in images built on this one; and xz-utils
# and bzip2, general-purpose tools that arrived with dpkg-dev. The checks after
# autoremove fail the build if any other src:gcc-NN binary survives or a compiler
# is still on PATH. The surviving runtime libraries are then upgraded, so a Debian
# fix for them ships on the next build.
USER root
RUN set -eu; \
    ul="$(dpkg-query -W -f='${source:Package} ${Package}\n' | awk '$1=="util-linux"{print $2}')"; \
    [ -n "$ul" ] || { echo "no src:util-linux packages found; refusing to skip the CVE patch" >&2; exit 1; }; \
    ex="$(dpkg-query -W -f='${source:Package} ${Package}\n' | awk '$1=="expat"{print $2}')"; \
    ossl="$(dpkg-query -W -f='${source:Package} ${Package}\n' | awk '$1=="openssl"{print $2}')"; \
    [ -n "$ossl" ] || { echo "no src:openssl packages found; refusing to skip the OpenSSL CVE patch" >&2; exit 1; }; \
    kh="$(dpkg-query -W -f='${source:Package} ${Package}\n' | awk '$1=="linux"{print $2}')"; \
    apt-get -qq update; \
    cve_pkgs="$(dpkg-query -W -f='${source:Package} ${Package}\n' | awk '$1 ~ /^(perl|glibc|sqlite3|gzip|libssh2|libevent|pcre2)$/ {print $2}')"; \
    apt-get -qq install -y --only-upgrade $ul $ex $kh $ossl $cve_pkgs; \
    apt-mark manual libpq5 libgomp1 xz-utils bzip2; \
    apt-get -qq purge -y libpq-dev libmariadb-dev libmariadb-dev-compat libmariadb3 mariadb-common libunbound8; \
    dpkg --purge libxml2 shared-mime-info libsasl2-dev libsasl2-modules libsasl2-modules-gssapi-mit \
      librdkafka-dev librdkafka1 'librdkafka++1'; \
    apt-get -qq purge -y build-essential gcc; \
    apt-get -qq autoremove -y --purge; \
    mariadb_left="$(dpkg-query -W -f='${source:Package} ${Package}\n' | awk '$1=="mariadb"{print $2}')"; \
    [ -z "$mariadb_left" ] || { echo "MariaDB packages survived the purge: $mariadb_left" >&2; exit 1; }; \
    cc_left="$(dpkg-query -W -f='${source:Package} ${Package}\n' | awk '($1=="gcc-defaults" || $1 ~ /^gcc-[0-9]+$/) && $2 !~ /^(gcc-[0-9]+-base|libgcc-s1|libstdc\+\+6|libgomp1)$/ {print $2}')"; \
    [ -z "$cc_left" ] || { echo "compiler packages survived the purge: $cc_left" >&2; exit 1; }; \
    for cc in cc c++ gcc g++ cpp; do if command -v "$cc" >/dev/null; then echo "$cc is still on PATH" >&2; exit 1; fi; done; \
    gcc_rt="$(dpkg-query -W -f='${source:Package} ${Package}\n' | awk '$1 ~ /^gcc-[0-9]+$/ {print $2}')"; \
    apt-get -qq install -y --only-upgrade $gcc_rt; \
    rm -rf /var/lib/apt/lists/*
USER openmetadata

# Fail the build if the purge above took a runtime library from an extension that was
# compiled earlier, or from the DB2 driver on amd64. Modules that a slimmer
# INGESTION_DEPENDENCY does not install are skipped.
RUN python -c "import importlib, importlib.util as u; [importlib.import_module(m) for m in ('psycopg2', 'kerberos', '_mysql_connector', 'ibm_db') if u.find_spec(m)]"
# The import check only covers modules we name. This catches any native library, in a wheel
# or a vendor driver, that links one of the libraries purged above.
RUN missing="$(find /home/openmetadata/.local /usr/local/lib /opt /instantclient -name '*.so*' -type f -exec ldd {} + 2>/dev/null \
      | grep -E '(libxml2|libsasl2|librdkafka|libmariadb|libmysqlclient)[^ ]* => not found' | sort -u)"; \
    [ -z "$missing" ] || { echo "native libraries link a purged library: $missing" >&2; exit 1; }

# Models are opt-in so the default image remains unchanged. Run this after the
# package installs and before the spaCy cleanup below, while using the runtime user.
ARG AUTO_CLASSIFICATION_LANGUAGES=""
RUN if [ -n "$AUTO_CLASSIFICATION_LANGUAGES" ]; then \
      metadata install-classification-models --languages "$AUTO_CLASSIFICATION_LANGUAGES"; \
    fi

# Strip spaCy's bundled test fixture, which scanners misreport as an installed black.
# See ingestion/scripts/strip_spacy_test_fixture.sh for the rationale. Must run after the
# final pip install, or a later install restores the fixture.
COPY --chown=openmetadata:openmetadata ingestion/scripts/strip_spacy_test_fixture.sh /tmp/strip_spacy_test_fixture.sh
RUN bash /tmp/strip_spacy_test_fixture.sh && rm -f /tmp/strip_spacy_test_fixture.sh

# Drop the teradatasql driver libraries this platform cannot load. The wheel ships
# all ten platform builds (337 MB of Go shared objects); only the two this
# architecture can load are kept, taking the package to 48 MB (arm64) / 70 MB (amd64);
# see ingestion/scripts/strip_teradatasql_arch_libs.sh. Same placement rule as the
# spaCy strip -- after the final pip install, or a later install restores them.
COPY --chown=openmetadata:openmetadata ingestion/scripts/strip_teradatasql_arch_libs.sh /tmp/strip_teradatasql_arch_libs.sh
RUN bash /tmp/strip_teradatasql_arch_libs.sh && rm -f /tmp/strip_teradatasql_arch_libs.sh
