This is an automated email from the ASF dual-hosted git repository.
yuqi1129 pushed a commit to branch branch-1.3
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/branch-1.3 by this push:
new 6ee0d06ea1 [Cherry-pick to branch-1.3] [#13265] feat(connector): Add
UBI-based Docker images for Trino, Flink and Spark connectors (#13266) (#13285)
6ee0d06ea1 is described below
commit 6ee0d06ea1d78b4242ee4a14bf002087cafddad4
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Fri Sep 18 09:09:24 2026 +0800
[Cherry-pick to branch-1.3] [#13265] feat(connector): Add UBI-based Docker
images for Trino, Flink and Spark connectors (#13266) (#13285)
**Cherry-pick Information:**
- Original commit: 2f93e96de125af7d981d7725b67c3bdf0ab84d2d
- Target branch: `branch-1.3`
- Status: ✅ Clean cherry-pick (no conflicts)
Co-authored-by: geyanggang <[email protected]>
---
.github/workflows/docker-image.yml | 12 +
dev/docker/build-docker.sh | 19 +-
dev/docker/flink-connectors/Dockerfile | 85 ++++++
dev/docker/flink-connectors/README.md | 210 ++++++++++++++
dev/docker/flink-connectors/copy-connector.sh | 75 +++++
.../flink-connectors-dependency.sh | 109 +++++++
dev/docker/flink-connectors/licenses/.gitignore | 4 +
.../licenses/THIRD_PARTY_LICENSES.txt | 20 ++
dev/docker/spark-connectors/Dockerfile | 87 ++++++
dev/docker/spark-connectors/README.md | 245 ++++++++++++++++
dev/docker/spark-connectors/copy-connector.sh | 87 ++++++
dev/docker/spark-connectors/licenses/.gitignore | 4 +
.../licenses/THIRD_PARTY_LICENSES.txt | 21 ++
.../spark-connectors-dependency.sh | 188 ++++++++++++
dev/docker/trino-connectors/Dockerfile | 85 ++++++
dev/docker/trino-connectors/README.md | 320 +++++++++++++++++++++
dev/docker/trino-connectors/copy-connector.sh | 122 ++++++++
dev/docker/trino-connectors/licenses/.gitignore | 4 +
.../licenses/THIRD_PARTY_LICENSES.txt | 24 ++
.../trino-connectors-dependency.sh | 100 +++++++
20 files changed, 1820 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/docker-image.yml
b/.github/workflows/docker-image.yml
index 0bd4f10d84..9b879557d2 100644
--- a/.github/workflows/docker-image.yml
+++ b/.github/workflows/docker-image.yml
@@ -21,6 +21,9 @@ on:
- 'gravitino-iceberg-rest-server'
- 'gravitino-mcp-server'
- 'gravitino-lance-rest-server'
+ - 'gravitino-trino-connector'
+ - 'gravitino-flink-connector'
+ - 'gravitino-spark-connector'
docker_repo_name:
description: 'Docker repository name (default is apache)'
required: false
@@ -99,6 +102,15 @@ jobs:
elif [ "${{ github.event.inputs.image }}" == "gravitino-mcp-server"
]; then
echo "image_type=mcp-server" >> $GITHUB_ENV
echo "image_name=${{ github.event.inputs.docker_repo_name
}}/gravitino-mcp-server" >> $GITHUB_ENV
+ elif [ "${{ github.event.inputs.image }}" ==
"gravitino-trino-connector" ]; then
+ echo "image_type=trino-connectors" >> $GITHUB_ENV
+ echo "image_name=${{ github.event.inputs.docker_repo_name
}}/gravitino-trino-connector" >> $GITHUB_ENV
+ elif [ "${{ github.event.inputs.image }}" ==
"gravitino-flink-connector" ]; then
+ echo "image_type=flink-connectors" >> $GITHUB_ENV
+ echo "image_name=${{ github.event.inputs.docker_repo_name
}}/gravitino-flink-connector" >> $GITHUB_ENV
+ elif [ "${{ github.event.inputs.image }}" ==
"gravitino-spark-connector" ]; then
+ echo "image_type=spark-connectors" >> $GITHUB_ENV
+ echo "image_name=${{ github.event.inputs.docker_repo_name
}}/gravitino-spark-connector" >> $GITHUB_ENV
fi
if [ "${{ github.event.inputs.publish-latest-tag }}" == "true" ];
then
diff --git a/dev/docker/build-docker.sh b/dev/docker/build-docker.sh
index d1c084185a..4f85738a10 100755
--- a/dev/docker/build-docker.sh
+++ b/dev/docker/build-docker.sh
@@ -27,7 +27,7 @@ usage() {
cat << EOF
Usage:
-./build-docker.sh --platform [all|linux/amd64|linux/arm64] --type
[gravitino|hive|kerberos-hive|trino|doris|ranger|iceberg-rest-server|lance-rest-server|mcp-server]
--image {image_name} --tag {tag_name} --latest
+./build-docker.sh --platform [all|linux/amd64|linux/arm64] --type
[gravitino|hive|kerberos-hive|trino|doris|ranger|iceberg-rest-server|lance-rest-server|mcp-server|trino-connectors|flink-connectors|spark-connectors]
--image {image_name} --tag {tag_name} --latest
Notice: You shouldn't use 'all' for the platform if you don't use the Github
action to publish the Docker image.
EOF
@@ -119,6 +119,23 @@ elif [ "${component_type}" == "mcp-server" ]; then
. ${script_dir}/mcp-server/mcp-server-dependency.sh
elif [ "${component_type}" == "lance-rest-server" ]; then
. ${script_dir}/lance-rest-server/lance-rest-server-dependency.sh
+elif [ "${component_type}" == "trino-connectors" ] || \
+ [ "${component_type}" == "flink-connectors" ] || \
+ [ "${component_type}" == "spark-connectors" ]; then
+ # Connector init-container images. Read the project version from
+ # gradle.properties so the Red Hat version LABEL is not hard-coded and stays
+ # correct across branches (main, branch-1.3, ...).
+ gravitino_root="$(cd "${script_dir}/../.." >/dev/null; pwd)"
+ image_version="$(grep -E '^version[[:space:]]*='
"${gravitino_root}/gradle.properties" | head -n1 | sed 's/.*=[[:space:]]*//' |
tr -d '[:space:]')"
+ if [ -z "${image_version}" ]; then
+ echo "ERROR : cannot read version from ${gravitino_root}/gradle.properties"
+ exit 1
+ fi
+ echo "INFO : IMAGE_VERSION=${image_version} (from gradle.properties)"
+ # Build the connector jars, stage them under packages/, and copy the
+ # repository-root LICENSE/NOTICE into licenses/ (done by the dependency
script).
+ . "${script_dir}/${component_type}/${component_type}-dependency.sh"
+ build_args="--build-arg IMAGE_VERSION=${image_version}"
else
echo "ERROR : ${component_type} is not a valid component type"
usage
diff --git a/dev/docker/flink-connectors/Dockerfile
b/dev/docker/flink-connectors/Dockerfile
new file mode 100644
index 0000000000..5448554747
--- /dev/null
+++ b/dev/docker/flink-connectors/Dockerfile
@@ -0,0 +1,85 @@
+#
+# 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.
+#
+
+#
==============================================================================
+# Apache Gravitino Flink Connector — UBI 10-based, certification-oriented image
+# Contains the Apache Gravitino Flink connector runtime jars for every
+# supported Flink version present in the source tree (Scala 2.12; Flink does
+# not support Scala 2.13). The connector code is Apache-2.0; the shaded runtime
+# jar bundles third-party open source components (see
+# licenses/THIRD_PARTY_LICENSES.txt).
+# Used as a Kubernetes init-container to inject connector jars into Flink pods.
+#
+# The set of Flink versions is discovered at build time from the Gravitino
+# source tree (see flink-connectors-dependency.sh), so this image tracks
+# whatever versions the checked-out branch supports without edits here.
+#
+# The Flink version is selected at runtime via the FLINK_VERSION env var.
+#
+# Red Hat UBI / certification-oriented properties:
+# - Based on UBI 10 (registry.access.redhat.com)
+# - Required LABELs present
+# - /licenses directory with LICENSE, NOTICE and THIRD_PARTY_LICENSES.txt
+# - Runs as non-root user (UID 1000, GID 0) with a passwd entry
+# - OpenShift arbitrary UID compatible (GID 0 group permissions)
+#
==============================================================================
+
+ARG UBI_MINIMAL_TAG=10.2
+# IMAGE_VERSION is injected at build time from gradle.properties by
+# build-docker.sh / the release workflow. The default below is kept in sync
+# with the current gradle.properties version as a fallback for uninjected
+# local builds.
+ARG IMAGE_VERSION=2.0.0-SNAPSHOT
+ARG IMAGE_RELEASE=1
+
+FROM registry.access.redhat.com/ubi10/ubi-minimal:${UBI_MINIMAL_TAG}
+ARG IMAGE_VERSION
+ARG IMAGE_RELEASE
+
+# --- Red Hat certification required LABELs ---
+LABEL name="gravitino-flink-connector" \
+ vendor="The Apache Software Foundation" \
+ version="${IMAGE_VERSION}" \
+ release="${IMAGE_RELEASE}" \
+ summary="Apache Gravitino Flink Connector (Scala 2.12)" \
+ description="Apache Gravitino Flink connector runtime jars for all
supported Flink versions (Scala 2.12), for Kubernetes init-container
deployment. The version is selected at runtime via the FLINK_VERSION
environment variable." \
+ maintainer="Apache Gravitino <[email protected]>"
+
+# Default Flink version (can be overridden at runtime)
+ENV FLINK_VERSION=1.20
+
+# Copy pre-built connector jars (prepared by flink-connectors-dependency.sh)
+COPY packages/connectors /connectors
+
+# Entrypoint script for version selection
+COPY --chmod=755 copy-connector.sh /copy-connector.sh
+
+# --- licenses directory (LICENSE + NOTICE + third-party summary) ---
+COPY licenses /licenses
+
+# --- Drop source-control artifacts, OpenShift arbitrary UID compatibility,
+# and a passwd entry for UID 1000 ---
+RUN rm -f /licenses/.gitignore \
+ && chgrp -R 0 /connectors /licenses \
+ && chmod -R g=u /connectors /licenses \
+ && echo "gravitino:x:1000:0:Gravitino user:/:/sbin/nologin" >> /etc/passwd
+
+USER 1000
+
+ENTRYPOINT ["/copy-connector.sh"]
diff --git a/dev/docker/flink-connectors/README.md
b/dev/docker/flink-connectors/README.md
new file mode 100644
index 0000000000..7df7dcfa90
--- /dev/null
+++ b/dev/docker/flink-connectors/README.md
@@ -0,0 +1,210 @@
+<!--
+ 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.
+-->
+
+# Apache Gravitino Flink Connector
+
+Copy this runtime JAR onto an existing Flink classpath, then configure Flink
+against an Apache Gravitino server.
+
+Image: `apache/gravitino-flink-connector:{version}`
+
+This image is not a Flink runtime. Use it as a Kubernetes init container, or
+unpack it on a VM, to copy one versioned JAR.
+
+## Build
+
+Build locally from the repository root with the shared image build script:
+
+```bash
+./dev/docker/build-docker.sh \
+ --platform linux/amd64 \
+ --type flink-connectors \
+ --image apache/gravitino-flink-connector \
+ --tag dev
+```
+
+The script builds the connector runtime jars
(`flink-connectors-dependency.sh`),
+stages the repository-root `LICENSE`/`NOTICE` into `licenses/`, injects
+`IMAGE_VERSION` from `gradle.properties`, and runs the multi-arch buildx build.
+
+This image contains open source software only. The Apache Gravitino connector
+code is licensed under the Apache License 2.0; the shaded runtime jar also
+bundles third-party open source components under their own licenses. See the
+`LICENSE`, `NOTICE` and `THIRD_PARTY_LICENSES.txt` files under `/licenses` in
+the image.
+
+## Server compatibility
+
+The connector must not be newer than the Gravitino server it connects to.
+`GravitinoClientBase` checks the version on the first metadata call, and a
+connector newer than the server fails that check; the failure surfaces as a
+catalog store that never loads. Match the connector image version to the
+server version, or keep it lower.
+
+## Supported versions
+
+Each Flink minor version ships as its own directory under `/connectors`
+(Scala 2.12; Flink does not support Scala 2.13). The exact set baked into an
+image depends on the Gravitino source branch it was built from; list them
+with:
+
+```bash
+docker run --rm apache/gravitino-flink-connector:{version}
+```
+
+Each directory contains one shaded runtime JAR. Do not mix JARs from
+different Flink minor versions. `LICENSE` and `NOTICE` are in `/licenses` in
+the image.
+
+## Install
+
+Mount `/target` and set `FLINK_VERSION`. The entrypoint copies the matching
+JAR into that directory.
+
+| Variable | Default | Description |
+|-----------------|----------|------------------------------------|
+| `FLINK_VERSION` | `1.20` | Flink minor version, e.g. `1.20`. |
+
+### Kubernetes
+
+Mount an empty volume at `/target` in the init container, and at
+`/opt/flink/usrlib` on the Flink main container. Do not mount over
+`/opt/flink/lib`. Match `FLINK_VERSION` to `spec.image` /
+`spec.flinkVersion`.
+
+```yaml
+volumes:
+ - name: flink-jars
+ emptyDir: {}
+initContainers:
+ - name: copy-gravitino-jars
+ image: apache/gravitino-flink-connector:{version}
+ env:
+ - name: FLINK_VERSION
+ value: "1.20"
+ volumeMounts:
+ - name: flink-jars
+ mountPath: /target
+containers:
+ - name: flink-main-container
+ volumeMounts:
+ - name: flink-jars
+ mountPath: /opt/flink/usrlib
+```
+
+To pick up a new connector image or settings, redeploy the `FlinkDeployment`
+so the init container runs again.
+
+### VM / on-premises
+
+Run the image once against the Flink `lib` directory, or unpack the JAR by
+hand:
+
+```bash
+docker run --rm \
+ -e FLINK_VERSION=1.20 \
+ -v /opt/flink/lib:/target \
+ apache/gravitino-flink-connector:{version}
+```
+
+Then restart JobManager and TaskManager so the catalog store loads.
+
+### Extra JARs
+
+This image ships only the connector runtime. JDBC catalogs also need, on the
+same classpath:
+
+- the Flink JDBC connector for that Flink minor, for example
+ `flink-connector-jdbc-3.3.0-1.20.jar` for Flink 1.20
+- the database JDBC driver
+
+For PostgreSQL catalogs, set `jdbc-database` and
+`flink.bypass.default-database` so `USE CATALOG` has a default database.
+
+## Configure
+
+Set these in `flink-conf.yaml`, `FlinkDeployment.spec.flinkConfiguration`, or
+`TableEnvironment`.
+
+Set `table.catalog-store.gravitino.gravitino.uri` to the server REST URL the
+engine can reach, then configure Basic or OAuth2. In the cluster this is
+usually `http://{service}.{namespace}.svc.cluster.local:8090`. A published
+HTTPS URL works the same way; it is not tied to the auth type.
+
+Auth type is `client.auth.type`. The OAuth2 token path key is `tokenPath`.
+
+### Basic
+
+```yaml
+table.catalog-store.kind: gravitino
+table.catalog-store.gravitino.gravitino.uri:
http://gravitino.example.svc.cluster.local:8090
+table.catalog-store.gravitino.gravitino.metalake: test
+table.catalog-store.gravitino.gravitino.client.auth.type: basic
+table.catalog-store.gravitino.gravitino.client.basic.username: admin
+table.catalog-store.gravitino.gravitino.client.basic.password: {password}
+```
+
+### OAuth2
+
+The connector authenticates with the client-credentials grant against any
+OAuth2 server. The example below uses Azure AD; a Keycloak realm token
+endpoint works the same way.
+
+```yaml
+table.catalog-store.kind: gravitino
+table.catalog-store.gravitino.gravitino.uri:
http://gravitino.example.svc.cluster.local:8090
+table.catalog-store.gravitino.gravitino.metalake: test
+table.catalog-store.gravitino.gravitino.client.auth.type: oauth2
+table.catalog-store.gravitino.gravitino.client.oauth2.serverUri:
https://login.microsoftonline.com
+table.catalog-store.gravitino.gravitino.client.oauth2.tokenPath:
{tenant_id}/oauth2/v2.0/token
+table.catalog-store.gravitino.gravitino.client.oauth2.credential:
{client_id}:{client_secret}
+table.catalog-store.gravitino.gravitino.client.oauth2.scope:
{client_id}/.default
+```
+
+### Properties
+
+| Property
| Required | Notes
|
+|-------------------------------------------------------------------------------------------------------------|----------|-------------------------------------|
+| `table.catalog-store.kind`
| Yes | Must be `gravitino`.
|
+| `table.catalog-store.gravitino.gravitino.uri`
| Yes | Server REST URL.
|
+| `table.catalog-store.gravitino.gravitino.metalake`
| Yes | Metalake name.
|
+| `table.catalog-store.gravitino.gravitino.client.auth.type`
| Yes | `basic` or `oauth2`.
|
+| `table.catalog-store.gravitino.gravitino.client.basic.username` /
`.password` | Basic | Basic credentials.
|
+| `table.catalog-store.gravitino.gravitino.client.oauth2.serverUri` /
`.tokenPath` / `.credential` / `.scope` | OAuth2 | OAuth2 client-credentials
settings. |
+
+Optional client settings use the prefix
+`table.catalog-store.gravitino.gravitino.client.`, for example
+`table.catalog-store.gravitino.gravitino.client.socketTimeoutMs`.
+
+## Catalog names
+
+Use the metalake catalog name directly, for example `catalog_postgres`. There
+is no `<metalake>.<catalog>` form. Names must not start with a digit.
+
+```sql
+SHOW CATALOGS;
+USE CATALOG catalog_postgres;
+SHOW DATABASES;
+SHOW TABLES FROM `public`;
+SELECT * FROM catalog_postgres.`public`.my_table LIMIT 20;
+```
+
+Session-only catalogs
+(`table.catalog-store.gravitino.gravitino.enableSessionCatalogSupport=true`)
+do not survive a redeploy.
diff --git a/dev/docker/flink-connectors/copy-connector.sh
b/dev/docker/flink-connectors/copy-connector.sh
new file mode 100755
index 0000000000..591aa7c6dc
--- /dev/null
+++ b/dev/docker/flink-connectors/copy-connector.sh
@@ -0,0 +1,75 @@
+#!/bin/bash
+#
+# 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.
+#
+
+# Copies the Apache Gravitino Flink connector jar matching FLINK_VERSION to
+# /target/.
+#
+# Environment variables:
+# FLINK_VERSION - Flink major version (default: 1.20)
+# LIST_VERSIONS - When "true", only list the available versions and exit.
+# Useful for `docker run --rm <image>`.
+#
+# The available versions are DISCOVERED at runtime from the directories baked
+# into /connectors (flink-<ver>). All versions are Scala 2.12 only (Flink does
+# not support Scala 2.13).
+#
+# As an init container, a missing /target volume is treated as an error so a
+# misconfigured pod fails fast instead of letting the engine start without the
+# connector. Set LIST_VERSIONS=true to only inspect the image.
+
+set -euo pipefail
+
+FLINK_VERSION="${FLINK_VERSION:-1.20}"
+LIST_VERSIONS="${LIST_VERSIONS:-false}"
+
+list_available_versions() {
+ ls -1 /connectors/ 2>/dev/null | grep "^flink-" | sed 's/flink-/ - /'
+}
+
+if [ "${LIST_VERSIONS}" = "true" ]; then
+ echo "Apache Gravitino Flink connector jars available at /connectors/ (all
Scala 2.12):"
+ echo ""
+ list_available_versions
+ echo ""
+ echo "Usage: mount a /target volume and set FLINK_VERSION (e.g.
FLINK_VERSION=1.20)."
+ exit 0
+fi
+
+if [ ! -d "/target" ]; then
+ echo "ERROR: /target volume is not mounted." >&2
+ echo "Mount an empty volume at /target so the connector jar can be
installed." >&2
+ echo "To only list versions, run with LIST_VERSIONS=true." >&2
+ exit 1
+fi
+
+SOURCE_DIR="/connectors/flink-${FLINK_VERSION}"
+
+if [ ! -d "$SOURCE_DIR" ]; then
+ echo "ERROR: Flink version ${FLINK_VERSION} is not supported by this image."
>&2
+ echo "" >&2
+ echo "Available versions (all Scala 2.12):" >&2
+ list_available_versions >&2
+ exit 1
+fi
+
+echo "Copying Flink ${FLINK_VERSION} connector (Scala 2.12) to /target/..."
+cp "${SOURCE_DIR}"/*.jar /target/
+echo "Done. Jars copied to /target/:"
+ls -1 /target/*.jar 2>/dev/null
diff --git a/dev/docker/flink-connectors/flink-connectors-dependency.sh
b/dev/docker/flink-connectors/flink-connectors-dependency.sh
new file mode 100755
index 0000000000..918d78235f
--- /dev/null
+++ b/dev/docker/flink-connectors/flink-connectors-dependency.sh
@@ -0,0 +1,109 @@
+#!/bin/bash
+#
+# 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.
+#
+
+# Build every Apache Gravitino Flink connector runtime shadow jar present in
+# the source tree and prepare the layout consumed by the Docker image.
+# Flink only supports Scala 2.12.
+#
+# The set of Flink runtime modules is DISCOVERED from the Gradle project, so
+# this script does not hard-code which versions exist. Whatever the checked-out
+# branch supports (for example 1.18/1.19/1.20) is built automatically.
+#
+# Output layout:
+#
packages/connectors/flink-<ver>/gravitino-flink-connector-runtime-<ver>_2.12-*.jar
+
+set -euo pipefail
+
+conn_dir="$(dirname "${BASH_SOURCE-$0}")"
+conn_dir="$(cd "${conn_dir}" >/dev/null; pwd)"
+gravitino_home="$(cd "${conn_dir}/../../.." >/dev/null; pwd)"
+
+cd "${gravitino_home}"
+
+# Discover all Flink runtime modules from the Gradle project,
+# e.g. "flink-runtime-1.20" -> version "1.20". Keep stderr so a Gradle failure
+# is visible instead of being misreported as "no modules found".
+runtime_modules="$(./gradlew -q projects | grep -oE
"flink-runtime-[0-9]+\.[0-9]+" | sort -u)"
+
+if [ -z "${runtime_modules}" ]; then
+ echo "ERROR: no flink-runtime modules found in the Gradle project." >&2
+ exit 1
+fi
+
+versions="$(echo "${runtime_modules}" | sed 's/flink-runtime-//' | sort -u)"
+
+echo "Discovered Flink connector versions:"
+echo "${versions}" | sed 's/^/ - /'
+
+# Assemble each discovered runtime shadow jar (Scala 2.12 only).
+tasks=""
+for m in ${runtime_modules}; do
+ tasks="${tasks} :flink-connector:${m}:shadowJar"
+done
+
+# shellcheck disable=SC2086
+./gradlew ${tasks} -x test
+
+# Clean old packages
+rm -rf "${conn_dir}/packages"
+mkdir -p "${conn_dir}/packages/connectors"
+
+# Copy shadow jars (exclude *-empty.jar artifacts) from each version's build
output.
+copied=0
+for ver in ${versions}; do
+ libs_dir="flink-connector/v${ver}/flink-runtime/build/libs"
+ dest="${conn_dir}/packages/connectors/flink-${ver}"
+ if [ -d "${libs_dir}" ]; then
+ mkdir -p "${dest}"
+ found=0
+ for jar in "${libs_dir}"/*.jar; do
+ [ -e "$jar" ] || continue
+ case "$jar" in
+ *-empty*) continue ;;
+ esac
+ cp "$jar" "${dest}/"
+ found=1
+ done
+ if [ "${found}" -eq 1 ]; then
+ copied=$((copied + 1))
+ else
+ echo "ERROR: no runtime jar found under ${libs_dir}" >&2
+ exit 1
+ fi
+ else
+ echo "ERROR: expected build output not found: ${libs_dir}" >&2
+ exit 1
+ fi
+done
+
+if [ "${copied}" -eq 0 ]; then
+ echo "ERROR: no Flink runtime jars were staged." >&2
+ exit 1
+fi
+
+# Stage the canonical Apache-2.0 LICENSE and NOTICE from the repository root so
+# the image ships the real texts (not drifting copies committed in-tree).
+cp "${gravitino_home}/LICENSE" "${conn_dir}/licenses/LICENSE"
+cp "${gravitino_home}/NOTICE" "${conn_dir}/licenses/NOTICE"
+
+echo ""
+echo "=== Flink connectors prepared (${copied} version(s)) ==="
+echo "Output: ${conn_dir}/packages/connectors/"
+find "${conn_dir}/packages/connectors/" -name "*.jar" | sort
diff --git a/dev/docker/flink-connectors/licenses/.gitignore
b/dev/docker/flink-connectors/licenses/.gitignore
new file mode 100644
index 0000000000..1ec3b21573
--- /dev/null
+++ b/dev/docker/flink-connectors/licenses/.gitignore
@@ -0,0 +1,4 @@
+# LICENSE and NOTICE are copied from the repository root at build time
+# by build-docker.sh / the CI workflow. Do not commit generated copies.
+/LICENSE
+/NOTICE
diff --git a/dev/docker/flink-connectors/licenses/THIRD_PARTY_LICENSES.txt
b/dev/docker/flink-connectors/licenses/THIRD_PARTY_LICENSES.txt
new file mode 100644
index 0000000000..461bc5f283
--- /dev/null
+++ b/dev/docker/flink-connectors/licenses/THIRD_PARTY_LICENSES.txt
@@ -0,0 +1,20 @@
+THIRD-PARTY SOFTWARE NOTICE — Apache Gravitino Flink Connector image
+
+This image packages the Apache Gravitino Flink connector as a shaded runtime
+jar. The shaded jar bundles third-party open source components, relocated
+under the org.apache.gravitino.shaded namespace and redistributed under their
+own licenses. They include, among others:
+
+ - Google Guava / Protobuf (com.google.*) ............... Apache-2.0 /
BSD-3-Clause
+ - Apache HttpComponents (org.apache.hc.*) .............. Apache-2.0
+ - Jackson (com.fasterxml.jackson.*) .................... Apache-2.0
+ - Caffeine (com.github.benmanes.caffeine) .............. Apache-2.0
+ - Apache Gravitino Java client ......................... Apache-2.0
+
+The Apache Gravitino connector code itself is licensed under the Apache
+License, Version 2.0 (see the LICENSE and NOTICE files in this directory).
+
+This list is a summary for operator convenience and is not exhaustive. The
+authoritative license and notice text for every bundled component is carried
+inside the jar's META-INF directory. Nothing in this summary modifies the
+license terms that apply to any individual component.
diff --git a/dev/docker/spark-connectors/Dockerfile
b/dev/docker/spark-connectors/Dockerfile
new file mode 100644
index 0000000000..395e26a412
--- /dev/null
+++ b/dev/docker/spark-connectors/Dockerfile
@@ -0,0 +1,87 @@
+#
+# 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.
+#
+
+#
==============================================================================
+# Apache Gravitino Spark Connector — UBI 10-based, certification-oriented image
+# Contains the Apache Gravitino Spark connector runtime jars for every
+# supported Spark version and Scala variant present in the source tree. The
+# connector code is Apache-2.0; the shaded runtime jar bundles third-party open
+# source components (see licenses/THIRD_PARTY_LICENSES.txt).
+# Used as a Kubernetes init-container to inject connector jars into Spark pods.
+#
+# The set of Spark version / Scala combinations is discovered at build time
+# from the Gravitino source tree (see spark-connectors-dependency.sh), so this
+# image tracks whatever the checked-out branch supports without edits here.
+#
+# The Spark and Scala versions are selected at runtime via SPARK_VERSION and
+# SCALA_VERSION env vars. When SCALA_VERSION is unset, the entrypoint defaults
+# to 2.13 for Spark 4.x (Scala 2.13 only) and 2.12 otherwise.
+#
+# Red Hat UBI / certification-oriented properties:
+# - Based on UBI 10 (registry.access.redhat.com)
+# - Required LABELs present
+# - /licenses directory with LICENSE, NOTICE and THIRD_PARTY_LICENSES.txt
+# - Runs as non-root user (UID 1000, GID 0) with a passwd entry
+# - OpenShift arbitrary UID compatible (GID 0 group permissions)
+#
==============================================================================
+
+ARG UBI_MINIMAL_TAG=10.2
+# IMAGE_VERSION is injected at build time from gradle.properties by
+# build-docker.sh / the release workflow. The default below is kept in sync
+# with the current gradle.properties version as a fallback for uninjected
+# local builds.
+ARG IMAGE_VERSION=2.0.0-SNAPSHOT
+ARG IMAGE_RELEASE=1
+
+FROM registry.access.redhat.com/ubi10/ubi-minimal:${UBI_MINIMAL_TAG}
+ARG IMAGE_VERSION
+ARG IMAGE_RELEASE
+
+# --- Red Hat certification required LABELs ---
+LABEL name="gravitino-spark-connector" \
+ vendor="The Apache Software Foundation" \
+ version="${IMAGE_VERSION}" \
+ release="${IMAGE_RELEASE}" \
+ summary="Apache Gravitino Spark Connector" \
+ description="Apache Gravitino Spark connector runtime jars for all
supported Spark versions and Scala variants, for Kubernetes init-container
deployment. The version is selected at runtime via the SPARK_VERSION and
SCALA_VERSION environment variables." \
+ maintainer="Apache Gravitino <[email protected]>"
+
+# Default Spark version (can be overridden at runtime). SCALA_VERSION is left
+# unset so the entrypoint can default it per Spark major (2.13 for Spark 4.x).
+ENV SPARK_VERSION=3.5
+
+# Copy pre-built connector jars (prepared by spark-connectors-dependency.sh)
+COPY packages/connectors /connectors
+
+# Entrypoint script for version selection
+COPY --chmod=755 copy-connector.sh /copy-connector.sh
+
+# --- licenses directory (LICENSE + NOTICE + third-party summary) ---
+COPY licenses /licenses
+
+# --- Drop source-control artifacts, OpenShift arbitrary UID compatibility,
+# and a passwd entry for UID 1000 ---
+RUN rm -f /licenses/.gitignore \
+ && chgrp -R 0 /connectors /licenses \
+ && chmod -R g=u /connectors /licenses \
+ && echo "gravitino:x:1000:0:Gravitino user:/:/sbin/nologin" >> /etc/passwd
+
+USER 1000
+
+ENTRYPOINT ["/copy-connector.sh"]
diff --git a/dev/docker/spark-connectors/README.md
b/dev/docker/spark-connectors/README.md
new file mode 100644
index 0000000000..78cafc7b61
--- /dev/null
+++ b/dev/docker/spark-connectors/README.md
@@ -0,0 +1,245 @@
+<!--
+ 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.
+-->
+
+# Apache Gravitino Spark Connector
+
+Copy this runtime JAR onto an existing Spark classpath, then configure Spark
+against an Apache Gravitino server.
+
+Image: `apache/gravitino-spark-connector:{version}`
+
+This image is not a Spark runtime. Use it as a Kubernetes init container, or
+unpack it on a VM, to copy one versioned JAR.
+
+## Build
+
+Build locally from the repository root with the shared image build script:
+
+```bash
+./dev/docker/build-docker.sh \
+ --platform linux/amd64 \
+ --type spark-connectors \
+ --image apache/gravitino-spark-connector \
+ --tag dev
+```
+
+The script builds the connector runtime jars
(`spark-connectors-dependency.sh`),
+stages the repository-root `LICENSE`/`NOTICE` into `licenses/`, injects
+`IMAGE_VERSION` from `gradle.properties`, and runs the multi-arch buildx build.
+
+This image contains open source software only. The Apache Gravitino connector
+code is licensed under the Apache License 2.0; the shaded runtime jar also
+bundles third-party open source components under their own licenses. See the
+`LICENSE`, `NOTICE` and `THIRD_PARTY_LICENSES.txt` files under `/licenses` in
+the image.
+
+## Server compatibility
+
+The connector must not be newer than the Gravitino server it connects to.
+`GravitinoClientBase` checks the version on the first metadata call, and a
+connector newer than the server fails that check; the failure surfaces as a
+catalog that never loads. Match the connector image version to the server
+version, or keep it lower.
+
+## Supported versions
+
+Each Spark version / Scala variant ships as its own directory under
+`/connectors` (named `spark-<major>_<scala>`). Across the project Spark 3.3 is
+Scala 2.12 only; Spark 3.4 and later add Scala 2.13; Spark 4.0 is Scala 2.13
+only. The exact set baked into an image depends on the Gravitino source branch
+it was built from; list them with:
+
+```bash
+docker run --rm apache/gravitino-spark-connector:{version}
+```
+
+Each directory contains one shaded runtime JAR. `LICENSE` and `NOTICE` are in
+`/licenses` in the image.
+
+## Install
+
+Mount `/target` and set the Spark and Scala versions. The entrypoint copies
+the matching JAR into that directory.
+
+| Variable | Default | Description
|
+|-----------------|---------|---------------------------------------------------|
+| `SPARK_VERSION` | `3.5` | Spark major version, e.g. `3.5`.
|
+| `SCALA_VERSION` | `2.12` | `2.12` or `2.13`, depending on the Spark
version. |
+
+### Kubernetes
+
+Mount an empty volume at `/target` in the init container, and at a path
+**outside** `/opt/spark/jars` on both driver and executor. Put that path on
+the classpath. Do not mount over `/opt/spark/jars` or `/opt/spark/conf`.
+
+```yaml
+volumes:
+ - name: spark-jars
+ emptyDir: {}
+driver:
+ volumeMounts:
+ - name: spark-jars
+ mountPath: /opt/gravitino/jars
+ initContainers:
+ - name: copy-gravitino-jars
+ image: apache/gravitino-spark-connector:{version}
+ env:
+ - name: SPARK_VERSION
+ value: "3.5"
+ - name: SCALA_VERSION
+ value: "2.12"
+ volumeMounts:
+ - name: spark-jars
+ mountPath: /target
+# Repeat the same volumeMount and initContainer on the executor.
+sparkConf:
+ "spark.plugins":
"org.apache.gravitino.spark.connector.plugin.GravitinoSparkPlugin"
+ "spark.driver.extraClassPath": "/opt/gravitino/jars/*"
+ "spark.executor.extraClassPath": "/opt/gravitino/jars/*"
+ "spark.sql.gravitino.uri": "http://gravitino.example.svc.cluster.local:8090"
+ "spark.sql.gravitino.metalake": "test"
+ "spark.sql.gravitino.authType": "basic"
+ "spark.sql.gravitino.basic.username": "admin"
+ "spark.sql.gravitino.basic.password": "{password}"
+```
+
+To pick up a new connector image or settings, submit a new job or roll the
+pods so the init container runs again.
+
+### VM / on-premises
+
+Run the image once against a classpath directory, or unpack the JAR by hand:
+
+```bash
+docker run --rm \
+ -e SPARK_VERSION=3.5 -e SCALA_VERSION=2.12 \
+ -v /opt/gravitino/jars:/target \
+ apache/gravitino-spark-connector:{version}
+```
+
+Then add that directory with `--jars` or via `spark.driver.extraClassPath`
+and `spark.executor.extraClassPath`.
+
+### The `spark.plugins` value is appended, not replaced
+
+Packaged Spark distributions often set `spark.plugins` already. A bare
+assignment silently disables the platform's own plugins. Append
+`GravitinoSparkPlugin` to any existing value (comma-separated) rather than
+overwriting it.
+
+### Extra JARs
+
+This image ships only the connector runtime.
+
+- JDBC catalogs need the database driver on the same classpath.
+- Iceberg catalogs need a matching `iceberg-spark-runtime` JAR and
+ `spark.sql.gravitino.enableIcebergSupport=true`.
+- Iceberg on S3 also needs `iceberg-aws-bundle` (matching the Iceberg
+ version) on the driver and executor classpath. `S3FileIO` uses AWS SDK v2
+ from that bundle; without it, the first use of vended credentials fails
+ with `NoClassDefFoundError`. Images that reach S3 only through Hadoop S3A
+ do not ship it.
+
+## Configure
+
+Set these in `spark-defaults.conf`, `spark-submit --conf`, or
+`SparkApplication.spec.sparkConf`.
+
+Set `spark.sql.gravitino.uri` to the server REST URL the engine can reach,
+then configure Basic or OAuth2. In the cluster this is usually
+`http://{service}.{namespace}.svc.cluster.local:8090`. A published HTTPS URL
+works the same way; it is not tied to the auth type.
+
+Note the two prefixes: auth settings are `spark.sql.gravitino.authType`,
+`spark.sql.gravitino.basic.*`, and `spark.sql.gravitino.oauth2.*` (no
+`client.` segment), while optional Gravitino client tuning uses
+`spark.sql.gravitino.client.`. Both match `GravitinoSparkConfig`. The OAuth2
+token path key is `spark.sql.gravitino.oauth2.tokenPath`.
+
+### Basic
+
+```properties
+spark.plugins=org.apache.gravitino.spark.connector.plugin.GravitinoSparkPlugin
+spark.sql.gravitino.uri=http://gravitino.example.svc.cluster.local:8090
+spark.sql.gravitino.metalake=test
+spark.sql.gravitino.authType=basic
+spark.sql.gravitino.basic.username=admin
+spark.sql.gravitino.basic.password={password}
+```
+
+### OAuth2
+
+The connector authenticates with the client-credentials grant against any
+OAuth2 server. The example below uses Azure AD; a Keycloak realm token
+endpoint works the same way.
+
+```properties
+spark.plugins=org.apache.gravitino.spark.connector.plugin.GravitinoSparkPlugin
+spark.sql.gravitino.uri=http://gravitino.example.svc.cluster.local:8090
+spark.sql.gravitino.metalake=test
+spark.sql.gravitino.authType=oauth2
+spark.sql.gravitino.oauth2.serverUri=https://login.microsoftonline.com
+spark.sql.gravitino.oauth2.tokenPath={tenant_id}/oauth2/v2.0/token
+spark.sql.gravitino.oauth2.credential={client_id}:{client_secret}
+spark.sql.gravitino.oauth2.scope={client_id}/.default
+```
+
+### Iceberg REST routing
+
+`hive` and `jdbc` backed Iceberg catalogs are routed through the Gravitino
+Iceberg REST server (IRC), enabled by default. The IRC endpoint is discovered
+from the server automatically.
+
+| Property | Default | Notes
|
+|----------------------------------------------------|--------------|------------------------------------------------------------------------------------------------------------------------------|
+| `spark.sql.gravitino.iceberg.rest-routing-enabled` | `true` | Route
Iceberg catalogs through IRC. Set `false` for legacy native-backend
translation. |
+| `spark.sql.gravitino.iceberg.rest-uri` | (discovered) | Override
the discovered IRC endpoint.
|
+| `spark.sql.gravitino.iceberg.reuseOAuth2` | `true` | Reuse
the Gravitino OAuth2 client config for IRC. Set `false` to supply an
independent IRC config or when IRC is not OAuth2. |
+| `spark.sql.gravitino.iceberg.rest.` | (none) |
Passthrough prefix for IRC client config, e.g. `rest.auth.type`.
|
+
+### Properties
+
+| Property
| Required | Notes
|
+|----------------------------------------------------------------------------------|----------|------------------------------------------------------------------------------|
+| `spark.plugins`
| Yes |
`org.apache.gravitino.spark.connector.plugin.GravitinoSparkPlugin` (append). |
+| `spark.sql.gravitino.uri`
| Yes | Server REST URL.
|
+| `spark.sql.gravitino.metalake`
| Yes | Metalake name.
|
+| `spark.sql.gravitino.authType`
| Yes | `simple`, `basic`, or `oauth2`.
|
+| `spark.sql.gravitino.basic.username` / `.password`
| Basic | Basic credentials.
|
+| `spark.sql.gravitino.oauth2.serverUri` / `.tokenPath` / `.credential` /
`.scope` | OAuth2 | OAuth2 client-credentials settings.
|
+| `spark.sql.gravitino.enableIcebergSupport`
| No | Set `true` for Iceberg catalogs.
|
+| `spark.sql.gravitino.iceberg.rest-routing-enabled`
| No | Default `true`.
|
+| `spark.sql.gravitino.iceberg.reuseOAuth2`
| No | Default `true`.
|
+
+Optional client settings use the prefix `spark.sql.gravitino.client.`, for
+example `spark.sql.gravitino.client.socketTimeoutMs`.
+
+## Catalog names
+
+Use the metalake catalog name directly, for example `catalog_postgres`. There
+is no `<metalake>.<catalog>` form.
+
+`SHOW CATALOGS` lists only `spark_catalog` until you `USE` another catalog.
+Prefer fully qualified names:
+
+```sql
+SHOW DATABASES IN catalog_postgres;
+SHOW TABLES IN catalog_postgres.public;
+SELECT * FROM catalog_postgres.public.my_table LIMIT 20;
+```
diff --git a/dev/docker/spark-connectors/copy-connector.sh
b/dev/docker/spark-connectors/copy-connector.sh
new file mode 100755
index 0000000000..9c33a787fd
--- /dev/null
+++ b/dev/docker/spark-connectors/copy-connector.sh
@@ -0,0 +1,87 @@
+#!/bin/bash
+#
+# 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.
+#
+
+# Copies the Apache Gravitino Spark connector jar matching SPARK_VERSION and
+# SCALA_VERSION to /target/.
+#
+# Environment variables:
+# SPARK_VERSION - Spark major version (default: 3.5)
+# SCALA_VERSION - Scala version. If unset, defaults to 2.13 for Spark 4.x
+# (which is Scala 2.13 only) and 2.12 otherwise.
+# LIST_VERSIONS - When "true", only list the available combinations and
+# exit. Useful for `docker run --rm <image>`.
+#
+# The available combinations are DISCOVERED at runtime from the directories
+# baked into /connectors (spark-<major>_<scala>), so this script needs no edits
+# when the matrix changes between branches.
+#
+# As an init container, a missing /target volume is treated as an error so a
+# misconfigured pod fails fast instead of letting the engine start without the
+# connector. Set LIST_VERSIONS=true to only inspect the image.
+
+set -euo pipefail
+
+SPARK_VERSION="${SPARK_VERSION:-3.5}"
+LIST_VERSIONS="${LIST_VERSIONS:-false}"
+
+# Default Scala per Spark major: Spark 4.x is Scala 2.13 only.
+if [ -z "${SCALA_VERSION:-}" ]; then
+ case "${SPARK_VERSION}" in
+ 4.*) SCALA_VERSION="2.13" ;;
+ *) SCALA_VERSION="2.12" ;;
+ esac
+fi
+
+SOURCE_DIR="/connectors/spark-${SPARK_VERSION}_${SCALA_VERSION}"
+
+list_available_combos() {
+ ls -1 /connectors/ 2>/dev/null | grep "^spark-" \
+ | sed 's/spark-/ Spark /' | sed 's/_/ + Scala /'
+}
+
+if [ "${LIST_VERSIONS}" = "true" ]; then
+ echo "Apache Gravitino Spark connector jars available at /connectors/:"
+ echo ""
+ list_available_combos
+ echo ""
+ echo "Usage: mount a /target volume and set SPARK_VERSION / SCALA_VERSION"
+ echo " (e.g. SPARK_VERSION=3.5 SCALA_VERSION=2.12)."
+ exit 0
+fi
+
+if [ ! -d "/target" ]; then
+ echo "ERROR: /target volume is not mounted." >&2
+ echo "Mount an empty volume at /target so the connector jar can be
installed." >&2
+ echo "To only list versions, run with LIST_VERSIONS=true." >&2
+ exit 1
+fi
+
+if [ ! -d "$SOURCE_DIR" ]; then
+ echo "ERROR: Spark ${SPARK_VERSION} with Scala ${SCALA_VERSION} is not
supported by this image." >&2
+ echo "" >&2
+ echo "Available combinations:" >&2
+ list_available_combos >&2
+ exit 1
+fi
+
+echo "Copying Spark ${SPARK_VERSION} connector (Scala ${SCALA_VERSION}) to
/target/..."
+cp "${SOURCE_DIR}"/*.jar /target/
+echo "Done. Jars copied to /target/:"
+ls -1 /target/*.jar 2>/dev/null
diff --git a/dev/docker/spark-connectors/licenses/.gitignore
b/dev/docker/spark-connectors/licenses/.gitignore
new file mode 100644
index 0000000000..1ec3b21573
--- /dev/null
+++ b/dev/docker/spark-connectors/licenses/.gitignore
@@ -0,0 +1,4 @@
+# LICENSE and NOTICE are copied from the repository root at build time
+# by build-docker.sh / the CI workflow. Do not commit generated copies.
+/LICENSE
+/NOTICE
diff --git a/dev/docker/spark-connectors/licenses/THIRD_PARTY_LICENSES.txt
b/dev/docker/spark-connectors/licenses/THIRD_PARTY_LICENSES.txt
new file mode 100644
index 0000000000..afd2b47fa8
--- /dev/null
+++ b/dev/docker/spark-connectors/licenses/THIRD_PARTY_LICENSES.txt
@@ -0,0 +1,21 @@
+THIRD-PARTY SOFTWARE NOTICE — Apache Gravitino Spark Connector image
+
+This image packages the Apache Gravitino Spark connector as a shaded runtime
+jar. The shaded jar bundles third-party open source components, relocated
+under the org.apache.gravitino.shaded namespace and redistributed under their
+own licenses. They include, among others:
+
+ - Google Guava / Protobuf (com.google.*) ............... Apache-2.0 /
BSD-3-Clause
+ - Apache HttpComponents (org.apache.hc.*) .............. Apache-2.0
+ - Jackson (com.fasterxml.jackson.*) .................... Apache-2.0
+ - Caffeine (com.github.benmanes.caffeine) .............. Apache-2.0
+ - Apache Kyuubi Spark connector (hive) ................. Apache-2.0
+ - Apache Gravitino Java client ......................... Apache-2.0
+
+The Apache Gravitino connector code itself is licensed under the Apache
+License, Version 2.0 (see the LICENSE and NOTICE files in this directory).
+
+This list is a summary for operator convenience and is not exhaustive. The
+authoritative license and notice text for every bundled component is carried
+inside the jar's META-INF directory. Nothing in this summary modifies the
+license terms that apply to any individual component.
diff --git a/dev/docker/spark-connectors/spark-connectors-dependency.sh
b/dev/docker/spark-connectors/spark-connectors-dependency.sh
new file mode 100755
index 0000000000..5021e1e942
--- /dev/null
+++ b/dev/docker/spark-connectors/spark-connectors-dependency.sh
@@ -0,0 +1,188 @@
+#!/bin/bash
+#
+# 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.
+#
+
+# Build every Apache Gravitino Spark connector runtime shadow jar present in
+# the source tree and prepare the layout consumed by the Docker image.
+#
+# The set of Spark runtime modules is DISCOVERED from the Gradle project, and
+# the Scala variants per module are determined by inspecting each module's
+# build script, so this script does not hard-code the version matrix. Whatever
+# the checked-out branch supports is built automatically:
+# - branch-1.3 : Spark 3.3 (2.12), 3.4 (2.12/2.13), 3.5 (2.12/2.13)
+# - main : Spark 3.5 (2.12/2.13), 4.0 (2.13 only, Scala-locked)
+#
+# Scala variant rules (derived, not hard-coded per version):
+# - A module whose build script hard-codes Scala (e.g. Spark 4.0 pins 2.13)
+# is built once, with no -PscalaVersion; the produced jar carries its own
+# Scala suffix.
+# - Otherwise the module is built for Scala 2.12, and additionally for 2.13
+# when the Spark major is >= 3.4 (Spark 3.3 is Scala 2.12 only across the
+# project).
+#
+# Output layout:
+#
packages/connectors/spark-<major>_<scala>/gravitino-spark-connector-runtime-<major>_<scala>-*.jar
+
+set -euo pipefail
+
+conn_dir="$(dirname "${BASH_SOURCE-$0}")"
+conn_dir="$(cd "${conn_dir}" >/dev/null; pwd)"
+gravitino_home="$(cd "${conn_dir}/../../.." >/dev/null; pwd)"
+
+cd "${gravitino_home}"
+
+# Discover all Spark runtime modules from the Gradle project,
+# e.g. "spark-runtime-3.5" -> major "3.5". Keep stderr so a Gradle failure is
+# visible instead of being misreported as "no modules found".
+runtime_modules="$(./gradlew -q projects | grep -oE
"spark-runtime-[0-9]+\.[0-9]+" | sort -u)"
+
+if [ -z "${runtime_modules}" ]; then
+ echo "ERROR: no spark-runtime modules found in the Gradle project." >&2
+ exit 1
+fi
+
+majors="$(echo "${runtime_modules}" | sed 's/spark-runtime-//' | sort -u)"
+
+echo "Discovered Spark runtime majors: $(echo "${majors}" | tr '\n' ' ')"
+
+# Locate a module's build.gradle.kts to inspect Scala handling.
+runtime_build_file() {
+ local major="$1"
+ echo "spark-connector/v${major}/spark-runtime/build.gradle.kts"
+}
+
+# Detect a Scala version hard-coded in the module build script, e.g.
+# val scalaVersion: String = "2.13"
+# Returns the pinned Scala version, or empty if the module reads
-PscalaVersion.
+detect_locked_scala() {
+ local build_file="$1"
+ [ -f "${build_file}" ] || { echo ""; return 0; }
+ grep -oE
'val[[:space:]]+scalaVersion[[:space:]]*:[[:space:]]*String[[:space:]]*=[[:space:]]*"[0-9]+\.[0-9]+"'
"${build_file}" \
+ | grep -oE '"[0-9]+\.[0-9]+"' | tr -d '"' | head -n1 || true
+}
+
+# Compare two dotted versions: returns 0 (true) if $1 >= $2.
+version_ge() {
+ [ "$(printf '%s\n%s\n' "$2" "$1" | sort -t. -k1,1n -k2,2n | tail -n1)" =
"$1" ]
+}
+
+# Build the list of "major:scala:kind" build targets, deriving Scala variants.
+targets=""
+for major in ${majors}; do
+ build_file="$(runtime_build_file "${major}")"
+ locked_scala="$(detect_locked_scala "${build_file}")"
+ if [ -n "${locked_scala}" ]; then
+ # Scala pinned by the module (e.g. Spark 4.0 -> 2.13). Build once.
+ targets="${targets} ${major}:${locked_scala}:locked"
+ else
+ # Flexible module: always Scala 2.12; add 2.13 for Spark major >= 3.4.
+ targets="${targets} ${major}:2.12:flag"
+ if version_ge "${major}" "3.4"; then
+ targets="${targets} ${major}:2.13:flag"
+ fi
+ fi
+done
+
+echo "Planned Spark build targets (major:scala):"
+for t in ${targets}; do echo " - ${t%:*}"; done
+
+# Run the gradle builds. Group flag-driven builds by Scala to minimise passes.
+flag_212_modules=""
+flag_213_modules=""
+locked_majors=""
+for t in ${targets}; do
+ major="$(echo "$t" | cut -d: -f1)"
+ scala="$(echo "$t" | cut -d: -f2)"
+ kind="$(echo "$t" | cut -d: -f3)"
+ case "${kind}" in
+ locked) locked_majors="${locked_majors} ${major}" ;;
+ flag)
+ if [ "${scala}" = "2.12" ]; then
+ flag_212_modules="${flag_212_modules}
:spark-connector:spark-runtime-${major}:shadowJar"
+ else
+ flag_213_modules="${flag_213_modules}
:spark-connector:spark-runtime-${major}:shadowJar"
+ fi
+ ;;
+ esac
+done
+
+if [ -n "${flag_212_modules}" ]; then
+ # shellcheck disable=SC2086
+ ./gradlew ${flag_212_modules} -PscalaVersion=2.12 -x test
+fi
+if [ -n "${flag_213_modules}" ]; then
+ # shellcheck disable=SC2086
+ ./gradlew ${flag_213_modules} -PscalaVersion=2.13 -x test
+fi
+for major in ${locked_majors}; do
+ # shellcheck disable=SC2086
+ ./gradlew :spark-connector:spark-runtime-${major}:shadowJar -x test
+done
+
+# Clean old packages
+rm -rf "${conn_dir}/packages"
+mkdir -p "${conn_dir}/packages/connectors"
+
+# Copy shadow jars into per-combination directories. The jar name embeds the
+# Scala suffix (…-runtime-<major>_<scala>-<ver>.jar), so we match on it and
+# never rely on a hard-coded matrix here.
+copy_variant() {
+ local major="$1" scala="$2"
+ local libs_dir="spark-connector/v${major}/spark-runtime/build/libs"
+ local dest="${conn_dir}/packages/connectors/spark-${major}_${scala}"
+ [ -d "${libs_dir}" ] || return 1
+ local found=0
+ for jar in "${libs_dir}"/*_"${scala}"-*.jar; do
+ [ -e "$jar" ] || continue
+ case "$jar" in
+ *-empty*) continue ;;
+ esac
+ mkdir -p "${dest}"
+ cp "$jar" "${dest}/"
+ found=1
+ done
+ [ "${found}" -eq 1 ]
+}
+
+copied=0
+for t in ${targets}; do
+ major="$(echo "$t" | cut -d: -f1)"
+ scala="$(echo "$t" | cut -d: -f2)"
+ if copy_variant "${major}" "${scala}"; then
+ copied=$((copied + 1))
+ else
+ echo "ERROR: no runtime jar found for Spark ${major} Scala ${scala}" >&2
+ exit 1
+ fi
+done
+
+if [ "${copied}" -eq 0 ]; then
+ echo "ERROR: no Spark runtime jars were staged." >&2
+ exit 1
+fi
+
+# Stage the canonical Apache-2.0 LICENSE and NOTICE from the repository root so
+# the image ships the real texts (not drifting copies committed in-tree).
+cp "${gravitino_home}/LICENSE" "${conn_dir}/licenses/LICENSE"
+cp "${gravitino_home}/NOTICE" "${conn_dir}/licenses/NOTICE"
+
+echo ""
+echo "=== Spark connectors prepared (${copied} combination(s)) ==="
+echo "Output: ${conn_dir}/packages/connectors/"
+find "${conn_dir}/packages/connectors/" -name "*.jar" | sort
diff --git a/dev/docker/trino-connectors/Dockerfile
b/dev/docker/trino-connectors/Dockerfile
new file mode 100644
index 0000000000..11cfcbefa6
--- /dev/null
+++ b/dev/docker/trino-connectors/Dockerfile
@@ -0,0 +1,85 @@
+#
+# 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.
+#
+
+#
==============================================================================
+# Apache Gravitino Trino Connector — UBI 10-based, certification-oriented image
+# Contains the Apache Gravitino Trino connector plugin for every supported
+# Trino/Starburst version range present in the source tree. Each band is a full
+# Trino plugin directory (connector jar + its dependency jars + LICENSE +
+# NOTICE + README). The connector code is Apache-2.0; the bundled dependency
+# jars are third-party open source (see licenses/THIRD_PARTY_LICENSES.txt).
+# Used as a Kubernetes init-container to inject connector jars into Trino pods.
+#
+# The set of version ranges is discovered at build time from the Gravitino
+# source tree (see trino-connectors-dependency.sh), so this image tracks
+# whatever versions the checked-out branch supports without edits here.
+#
+# The Trino version range is selected at runtime via the TRINO_VERSION env var.
+#
+# Red Hat UBI / certification-oriented properties:
+# - Based on UBI 10 (registry.access.redhat.com)
+# - Required LABELs present
+# - /licenses directory with LICENSE, NOTICE and THIRD_PARTY_LICENSES.txt
+# - Runs as non-root user (UID 1000, GID 0) with a passwd entry
+# - OpenShift arbitrary UID compatible (GID 0 group permissions)
+#
==============================================================================
+
+ARG UBI_MINIMAL_TAG=10.2
+# IMAGE_VERSION is injected at build time from gradle.properties by
+# build-docker.sh / the release workflow. The default below is kept in sync
+# with the current gradle.properties version as a fallback for uninjected
+# local builds.
+ARG IMAGE_VERSION=2.0.0-SNAPSHOT
+ARG IMAGE_RELEASE=1
+
+FROM registry.access.redhat.com/ubi10/ubi-minimal:${UBI_MINIMAL_TAG}
+ARG IMAGE_VERSION
+ARG IMAGE_RELEASE
+
+# --- Red Hat certification required LABELs ---
+LABEL name="gravitino-trino-connector" \
+ vendor="The Apache Software Foundation" \
+ version="${IMAGE_VERSION}" \
+ release="${IMAGE_RELEASE}" \
+ summary="Apache Gravitino Trino/Starburst Connector" \
+ description="Apache Gravitino Trino connector for all supported
Trino/Starburst version ranges, for Kubernetes init-container deployment.
Supports open-source Trino and Starburst. The version range is selected at
runtime via the TRINO_VERSION environment variable." \
+ maintainer="Apache Gravitino <[email protected]>"
+
+# Default Trino version (can be overridden at runtime, e.g. 478, 452, 440)
+ENV TRINO_VERSION=478
+
+# Copy pre-built connector jars (prepared by trino-connectors-dependency.sh)
+COPY packages/connectors /connectors
+
+# Entrypoint script for version selection
+COPY --chmod=755 copy-connector.sh /copy-connector.sh
+
+# --- licenses directory (LICENSE + NOTICE + third-party summary) ---
+COPY licenses /licenses
+
+# --- Drop source-control artifacts, OpenShift arbitrary UID compatibility,
+# and a passwd entry for UID 1000 ---
+RUN rm -f /licenses/.gitignore \
+ && chgrp -R 0 /connectors /licenses \
+ && chmod -R g=u /connectors /licenses \
+ && echo "gravitino:x:1000:0:Gravitino user:/:/sbin/nologin" >> /etc/passwd
+
+USER 1000
+
+ENTRYPOINT ["/copy-connector.sh"]
diff --git a/dev/docker/trino-connectors/README.md
b/dev/docker/trino-connectors/README.md
new file mode 100644
index 0000000000..64dedcec63
--- /dev/null
+++ b/dev/docker/trino-connectors/README.md
@@ -0,0 +1,320 @@
+<!--
+ 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.
+-->
+
+# Apache Gravitino Trino Connector
+
+Copy this plugin into an existing Trino or Starburst installation, then
+configure it against an Apache Gravitino server.
+
+Image: `apache/gravitino-trino-connector:{version}`
+
+This image is not a Trino server. Use it as a Kubernetes init container, or
+unpack it on a VM, to place one version band under Trino's `plugin/`
+directory.
+
+## Build
+
+Build locally from the repository root with the shared image build script:
+
+```bash
+./dev/docker/build-docker.sh \
+ --platform linux/amd64 \
+ --type trino-connectors \
+ --image apache/gravitino-trino-connector \
+ --tag dev
+```
+
+The script builds the connector bands (`trino-connectors-dependency.sh`),
+stages the repository-root `LICENSE`/`NOTICE` into `licenses/`, injects
+`IMAGE_VERSION` from `gradle.properties`, and runs the multi-arch buildx build.
+
+This image contains open source software only. The Apache Gravitino connector
+code is licensed under the Apache License 2.0; each plugin band also bundles
+third-party open source dependency jars under their own licenses. See the
+`LICENSE`, `NOTICE` and `THIRD_PARTY_LICENSES.txt` files under `/licenses` in
+the image.
+
+## Server compatibility
+
+The connector must not be newer than the Gravitino server it connects to.
+`GravitinoClientBase` checks the version on the first metadata poll, and a
+connector newer than the server fails that check inside `loadMetalake`; the
+failure is swallowed, so no catalog appears and nothing is logged. Match the
+connector image version to the server version, or keep it lower.
+
+On an unsupported Trino version, set
+`gravitino.trino.skip-version-validation=true` to load anyway (untested).
+
+## Supported versions
+
+Each Trino/Starburst version band ships as its own plugin directory under
+`/connectors`. The exact set of bands baked into an image depends on the
+Gravitino source branch it was built from; list them with:
+
+```bash
+docker run --rm apache/gravitino-trino-connector:{version}
+```
+
+Each band is a full plugin directory: the connector JAR, its dependencies,
+`LICENSE`, `NOTICE`, and this README. Install **exactly one** band.
+
+## JDBC drivers
+
+This image ships **no** JDBC drivers, by design. A Gravitino JDBC catalog
+(for example MySQL or PostgreSQL) is served by Trino's own `mysql` /
+`postgresql` connector, and the JDBC driver comes from that Trino plugin
+(`plugin/mysql`, `plugin/postgresql`) — not from this image. Standard Trino and
+Starburst distributions already include those plugins, so no driver setup is
+needed.
+
+If you run your own Trino and have removed the `mysql` or `postgresql` plugin,
+reinstall it (that is where the driver lives); a driver placed in the Gravitino
+plugin directory would not be used, as Trino isolates each plugin's classpath.
+
+## Install
+
+Mount `/target` and set `TRINO_VERSION`. The entrypoint copies the matching
+band into that directory.
+
+| Variable | Default | Description
|
+|-----------------|---------|---------------------------------------------------------|
+| `TRINO_VERSION` | `478` | Trino server version. Resolved to a band
automatically. |
+
+### Kubernetes
+
+Mount an empty volume at `/target` in the init container, and at the plugin
+path on the coordinator and every worker. The last path component must be
+`gravitino`.
+
+The usual plugin path on the official Helm chart is
+`/usr/lib/trino/plugin/gravitino`.
+
+```yaml
+volumes:
+ - name: gravitino-plugin
+ emptyDir: {}
+initContainers:
+ - name: install-gravitino-connector
+ image: apache/gravitino-trino-connector:{version}
+ env:
+ - name: TRINO_VERSION
+ value: "477"
+ volumeMounts:
+ - name: gravitino-plugin
+ mountPath: /target
+containers:
+ - name: trino
+ volumeMounts:
+ - name: gravitino-plugin
+ mountPath: /usr/lib/trino/plugin/gravitino
+```
+
+To pick up a new connector image or catalog properties, roll the coordinator
+and worker pods so the init container runs again.
+
+### VM / on-premises
+
+Run the image once against the target plugin directory, or unpack a band by
+hand:
+
+```bash
+docker run --rm \
+ -e TRINO_VERSION=477 \
+ -v /usr/lib/trino/plugin/gravitino:/target \
+ apache/gravitino-trino-connector:{version}
+```
+
+The directory name under `plugin/` must be `gravitino`. Repeat on the
+coordinator and every worker, then restart each Trino process.
+
+### Trino server settings
+
+On the coordinator:
+
+```properties
+catalog.management=dynamic
+```
+
+On Kubernetes, when catalogs are stored on disk, also set on the coordinator:
+
+```properties
+catalog.store=file
+```
+
+### Logging
+
+The connector emits nothing at any level unless the coordinator JVM is
+started with a Log4j 2 configuration file:
+
+```
+-Dlog4j.configurationFile=/etc/trino/log4j2.properties
+```
+
+Without it, the version and registration failures above are invisible.
+
+## Configure
+
+Create the Trino catalog file
+`/etc/trino/catalog/gravitino.properties`. On the official Helm chart, set
+the same keys under `catalogs.gravitino`; the chart writes that file. The
+file name (`gravitino`) is the Trino catalog that hosts the connector. It is
+not the plugin directory under `/usr/lib/trino/plugin/gravitino`.
+
+Set `gravitino.uri` to the server REST URL the engine can reach, then
+configure Basic or OAuth2. In the cluster this is usually
+`http://{service}.{namespace}.svc.cluster.local:8090`. A published HTTPS URL
+works the same way; it is not tied to the auth type.
+
+`gravitino.client.authType` accepts `simple`, `basic`, `oauth2`, or
+`kerberos`. The OAuth2 token path key is `gravitino.client.oauth2.path`.
+
+### Basic
+
+```properties
+connector.name=gravitino
+gravitino.uri=http://gravitino.example.svc.cluster.local:8090
+gravitino.metalake=test
+gravitino.use-single-metalake=false
+gravitino.client.authType=basic
+gravitino.client.basic.username=admin
+gravitino.client.basic.password={password}
+```
+
+### OAuth2
+
+The connector authenticates with the client-credentials grant against any
+OAuth2 server. The example below uses Azure AD; a Keycloak realm token
+endpoint works the same way.
+
+```properties
+connector.name=gravitino
+gravitino.uri=http://gravitino.example.svc.cluster.local:8090
+gravitino.metalake=test
+gravitino.use-single-metalake=false
+gravitino.client.authType=oauth2
+gravitino.client.oauth2.serverUri=https://login.microsoftonline.com
+gravitino.client.oauth2.credential={client_id}:{client_secret}
+gravitino.client.oauth2.path={tenant_id}/oauth2/v2.0/token
+gravitino.client.oauth2.scope={client_id}/.default
+```
+
+### Worker credentials on a distributed cluster
+
+The connector registers catalogs by issuing `CREATE CATALOG` over JDBC to the
+coordinator. Secret values are stripped from the catalog definition sent to
+workers, so a literal password or credential leaves distributed queries
+failing with `REMOTE_TASK_ERROR`. Reference secrets by environment variable
+instead: map a property to an env var with the
+`gravitino.dynamic-catalog.environment-variable.` prefix, and the connector
+writes `${ENV:...}` into the worker catalog definition.
+
+```properties
+# Property -> env var. The connector emits the property as '${ENV:VAR}' in the
worker catalog.
+gravitino.dynamic-catalog.environment-variable.gravitino.client.oauth2.credential=GRAVITINO_CLIENT_CREDENTIAL
+gravitino.dynamic-catalog.environment-variable.gravitino.iceberg.rest-catalog.oauth2.credential=IRC_CLIENT_CREDENTIAL
+```
+
+Set the matching env vars (`GRAVITINO_CLIENT_CREDENTIAL`, etc.) on the
+coordinator and every worker.
+
+### Connecting to a TLS-enabled coordinator
+
+The internal JDBC connection to the coordinator must trust the coordinator's
+certificate, or `CREATE CATALOG` fails with a PKIX error and no catalogs
+appear. Configure the `trino.jdbc.*` properties in the same catalog file:
+
+```properties
+trino.jdbc.user=admin
+trino.jdbc.password={trino_password}
+trino.jdbc.ssl.truststore.path=/etc/trino/truststore.jks
+trino.jdbc.ssl.truststore.password={truststore_password}
+# Required when the deployment only allows CREATE CATALOG with a privileged
role.
+trino.jdbc.roles=system:sysadmin
+```
+
+`trino.jdbc.ssl.enabled` may be omitted when `discovery.uri` uses `https`; it
+is derived from that scheme. The `trino.jdbc.*` values are used by the
+coordinator only and are never copied into the catalogs the connector
+creates.
+
+### Iceberg REST routing
+
+`lakehouse-iceberg` catalogs are routed through the Gravitino Iceberg REST
+server (IRC), enabled by default. The IRC endpoint is discovered from the
+server automatically.
+
+| Property | Default | Notes
|
+|------------------------------------------|--------------|-----------------------------------------------------------------------------------------------------------------|
+| `gravitino.iceberg.rest-routing-enabled` | `true` | Route non-REST
Iceberg catalogs through IRC. Set `false` for legacy catalog-backend
translation. |
+| `gravitino.iceberg.rest-uri` | (discovered) | Override the
discovered IRC endpoint. In multi-metalake mode use
`gravitino.iceberg.rest-uri.{metalake}`. |
+| `gravitino.iceberg.rest-catalog.` | (none) | Passthrough prefix
rewritten to `iceberg.rest-catalog.`, e.g.
`gravitino.iceberg.rest-catalog.security=OAUTH2`. |
+
+### Identity forwarding
+
+To have the Gravitino server authorize each end user instead of the shared
+service identity, forward the Trino session user:
+
+| Property | Default | Notes
|
+|---------------------------------------------------|---------|-----------------------------------------------------------------------------|
+| `gravitino.client.session.forwardUser` | `false` | Per-session
Gravitino client. Supported with `authType=simple` or `oauth2`. |
+| `gravitino.client.session.userTokenCredentialKey` | `token` |
Extra-credential key carrying the forwarded OAuth2 token. |
+
+### Properties
+
+| Property |
Required | Notes |
+|--------------------------------------------------------------------------|----------|-----------------------------------------------------------|
+| `connector.name` |
Yes | Must be `gravitino`. |
+| `gravitino.metalake` |
Yes | Metalake on the server. |
+| `gravitino.uri` |
Yes | Server REST URL. |
+| `gravitino.use-single-metalake` |
No | Default `true`. See catalog names. |
+| `gravitino.client.authType` |
Yes | `simple`, `basic`, `oauth2`, or `kerberos`. |
+| `gravitino.client.basic.username` / `.password` |
Basic | Basic credentials. |
+| `gravitino.client.oauth2.serverUri` / `.path` / `.credential` / `.scope` |
OAuth2 | OAuth2 client-credentials settings. |
+| `trino.jdbc.user` / `.password` |
No | Internal JDBC connection to the coordinator. |
+| `trino.jdbc.ssl.enabled` |
No | Derived from `discovery.uri` scheme when unset. |
+| `trino.jdbc.ssl.truststore.path` / `.password` / `.type` |
No | Coordinator certificate trust. |
+| `trino.jdbc.ssl.keystore.path` / `.password` / `.type` |
No | Client certificate for mutual TLS. |
+| `trino.jdbc.ssl.verification` |
No | `FULL` (default), `CA`, or `NONE` (troubleshooting only). |
+| `trino.jdbc.roles` |
No | Session roles, e.g. `system:sysadmin`. |
+| `trino.jdbc.properties.` |
No | Passthrough prefix for arbitrary JDBC driver properties. |
+| `gravitino.iceberg.rest-routing-enabled` |
No | Default `true`. |
+| `gravitino.iceberg.rest-uri` |
No | Override discovered IRC endpoint. |
+| `gravitino.iceberg.rest-catalog.` |
No | IRC passthrough prefix. |
+| `gravitino.client.session.forwardUser` |
No | Forward the session user. |
+| `gravitino.trino.skip-version-validation` |
No | Default `false`. |
+
+Optional client settings use the prefix `gravitino.client.`, for example
+`gravitino.client.socketTimeoutMs`.
+
+New metalake catalogs can appear after the plugin is loaded, on the refresh
+interval (`gravitino.metadata.refresh-interval-seconds`, default `10`).
+
+## Catalog names
+
+- `gravitino.use-single-metalake=true` (default): `<catalog_name>`, for
+ example `hive`.
+- `gravitino.use-single-metalake=false`: `<metalake_name>.<catalog_name>`,
+ for example `test.hive`.
+
+`gravitino` is the connector catalog. Metalake catalogs are registered
+separately.
+
+```sql
+SHOW CATALOGS;
+```
diff --git a/dev/docker/trino-connectors/copy-connector.sh
b/dev/docker/trino-connectors/copy-connector.sh
new file mode 100755
index 0000000000..aad4af1360
--- /dev/null
+++ b/dev/docker/trino-connectors/copy-connector.sh
@@ -0,0 +1,122 @@
+#!/bin/bash
+#
+# 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.
+#
+
+# Copies the Apache Gravitino Trino connector jars matching TRINO_VERSION to
+# /target/.
+#
+# Environment variables:
+# TRINO_VERSION - Specific Trino version number (default: 478), or an exact
+# range directory name such as "473-478".
+# LIST_VERSIONS - When "true", only list the available version ranges and
+# exit. Useful for `docker run --rm <image>`.
+#
+# The supported version ranges are DISCOVERED at runtime from the directories
+# baked into /connectors (trino-<lo>-<hi>), so this script needs no edits when
+# the set of ranges changes between branches.
+#
+# As an init container, a missing /target volume is treated as an error so a
+# misconfigured pod fails fast instead of letting the engine start with an
+# empty plugin directory. Set LIST_VERSIONS=true to only inspect the image.
+
+set -euo pipefail
+
+TRINO_VERSION="${TRINO_VERSION:-478}"
+LIST_VERSIONS="${LIST_VERSIONS:-false}"
+
+list_available_ranges() {
+ ls -1 /connectors/ 2>/dev/null | grep "^trino-" | sed 's/trino-/ - /'
+}
+
+# Resolve a specific Trino version number to the matching connector range
+# directory by inspecting the ranges actually present under /connectors.
+# Prints the resolved range (empty if unsupported) and always returns 0, so it
+# is safe under `set -e` in a command substitution.
+resolve_version_range() {
+ local version="$1"
+ local dir range lo hi
+
+ # Exact range directory name passed directly, e.g. "473-478".
+ if [ -d "/connectors/trino-${version}" ]; then
+ echo "${version}"
+ return 0
+ fi
+
+ # A specific numeric version: find the range [lo,hi] that contains it.
+ if echo "${version}" | grep -qE '^[0-9]+$'; then
+ for dir in /connectors/trino-*; do
+ [ -d "$dir" ] || continue
+ range="$(basename "$dir" | sed 's/^trino-//')"
+ lo="${range%-*}"
+ hi="${range#*-}"
+ case "${lo}${hi}" in
+ *[!0-9]*) continue ;;
+ esac
+ if [ "$version" -ge "$lo" ] && [ "$version" -le "$hi" ]; then
+ echo "${range}"
+ return 0
+ fi
+ done
+ fi
+
+ echo ""
+ return 0
+}
+
+if [ "${LIST_VERSIONS}" = "true" ]; then
+ echo "Apache Gravitino Trino/Starburst connector jars available at
/connectors/"
+ echo ""
+ echo "Available connector ranges:"
+ list_available_ranges
+ echo ""
+ echo "Usage: mount a /target volume and set TRINO_VERSION (e.g.
TRINO_VERSION=478)."
+ exit 0
+fi
+
+if [ ! -d "/target" ]; then
+ echo "ERROR: /target volume is not mounted." >&2
+ echo "Mount an empty volume at /target (the engine plugin directory) so the"
>&2
+ echo "connector can be installed. To only list versions, run with
LIST_VERSIONS=true." >&2
+ exit 1
+fi
+
+VERSION_RANGE="$(resolve_version_range "$TRINO_VERSION")"
+
+if [ -z "$VERSION_RANGE" ]; then
+ echo "ERROR: Trino version ${TRINO_VERSION} is not supported by this image."
>&2
+ echo "" >&2
+ echo "Available connector ranges:" >&2
+ list_available_ranges >&2
+ exit 1
+fi
+
+SOURCE_DIR="/connectors/trino-${VERSION_RANGE}"
+
+if [ ! -d "$SOURCE_DIR" ]; then
+ echo "ERROR: Connector directory not found: ${SOURCE_DIR}" >&2
+ exit 1
+fi
+
+echo "Trino version ${TRINO_VERSION} resolved to connector range:
${VERSION_RANGE}"
+echo "Copying connector jars to /target/..."
+cp -r "${SOURCE_DIR}"/* /target/
+
+echo ""
+echo "Done. Files in /target/:"
+find /target -name "*.jar" | sort
diff --git a/dev/docker/trino-connectors/licenses/.gitignore
b/dev/docker/trino-connectors/licenses/.gitignore
new file mode 100644
index 0000000000..1ec3b21573
--- /dev/null
+++ b/dev/docker/trino-connectors/licenses/.gitignore
@@ -0,0 +1,4 @@
+# LICENSE and NOTICE are copied from the repository root at build time
+# by build-docker.sh / the CI workflow. Do not commit generated copies.
+/LICENSE
+/NOTICE
diff --git a/dev/docker/trino-connectors/licenses/THIRD_PARTY_LICENSES.txt
b/dev/docker/trino-connectors/licenses/THIRD_PARTY_LICENSES.txt
new file mode 100644
index 0000000000..3b8cb1f550
--- /dev/null
+++ b/dev/docker/trino-connectors/licenses/THIRD_PARTY_LICENSES.txt
@@ -0,0 +1,24 @@
+THIRD-PARTY SOFTWARE NOTICE — Apache Gravitino Trino Connector image
+
+This image packages the Apache Gravitino Trino connector plugin. Each Trino
+version band under /connectors is a Trino plugin directory that contains the
+Gravitino connector jar together with its runtime dependency jars.
+
+Those dependency jars are third-party open source components, redistributed
+under their own licenses. They include, among others:
+
+ - Airlift libraries (log, etc.) ......................... Apache-2.0
+ - Google Guava ......................................... Apache-2.0
+ - Apache Commons (collections4, lang3, ...) ............ Apache-2.0
+ - Jackson (com.fasterxml.jackson.*) .................... Apache-2.0
+ - OpenTelemetry semantic conventions ................... Apache-2.0
+ - Trino JDBC client (io.trino:trino-jdbc) .............. Apache-2.0
+
+The Apache Gravitino connector jar itself is licensed under the Apache
+License, Version 2.0 (see the LICENSE and NOTICE files in this directory and
+in each plugin band directory).
+
+This list is a summary for operator convenience and is not exhaustive. The
+authoritative license and notice text for every bundled component is carried
+inside the respective jar's META-INF directory. Nothing in this summary
+modifies the license terms that apply to any individual component.
diff --git a/dev/docker/trino-connectors/trino-connectors-dependency.sh
b/dev/docker/trino-connectors/trino-connectors-dependency.sh
new file mode 100755
index 0000000000..3e6f592402
--- /dev/null
+++ b/dev/docker/trino-connectors/trino-connectors-dependency.sh
@@ -0,0 +1,100 @@
+#!/bin/bash
+#
+# 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.
+#
+
+# Build every Apache Gravitino Trino connector version range present in the
+# source tree and prepare the layout consumed by the Docker image.
+#
+# The set of version-range modules is DISCOVERED from the Gradle project, so
+# this script does not hard-code which ranges exist. Whatever the checked-out
+# branch supports (for example 440-478 on main, or 435-478 on branch-1.3) is
+# built automatically.
+#
+# Output layout:
+# packages/connectors/trino-<range>/ (plugin dir: jars + LICENSE + NOTICE
+ README)
+#
+# Trino ships its own MySQL and PostgreSQL drivers inside its mysql and
+# postgresql plugins, so no JDBC drivers are bundled here.
+
+set -euo pipefail
+
+conn_dir="$(dirname "${BASH_SOURCE-$0}")"
+conn_dir="$(cd "${conn_dir}" >/dev/null; pwd)"
+gravitino_home="$(cd "${conn_dir}/../../.." >/dev/null; pwd)"
+
+cd "${gravitino_home}"
+
+# Discover all Trino connector version-range modules from the Gradle project,
+# e.g. "trino-connector-440-445". Keep stderr so a Gradle failure is visible
+# instead of being misreported as "no modules found".
+modules="$(./gradlew -q projects | grep -oE "trino-connector-[0-9]+-[0-9]+" |
sort -u)"
+
+if [ -z "${modules}" ]; then
+ echo "ERROR: no trino-connector version-range modules found in the Gradle
project." >&2
+ exit 1
+fi
+
+echo "Discovered Trino connector modules:"
+echo "${modules}" | sed 's/^/ - /'
+
+# Assemble each discovered version range.
+tasks=""
+for m in ${modules}; do
+ tasks="${tasks} :trino-connector:${m}:assembleTrinoConnector"
+done
+
+# shellcheck disable=SC2086
+./gradlew ${tasks} -x test
+
+# Clean old packages
+rm -rf "${conn_dir}/packages"
+mkdir -p "${conn_dir}/packages/connectors"
+
+# The assembleTrinoConnector task produces, per module, a plugin directory at
+# distribution/gravitino-trino-connector-<range>/ (jars + LICENSE + NOTICE +
README).
+# Copy ONLY the ranges built in this run (derived from the discovered modules),
+# so leftover distribution/ directories from previous builds are never picked
up.
+copied=0
+for m in ${modules}; do
+ range="${m#trino-connector-}"
+ dir="distribution/gravitino-trino-connector-${range}"
+ if [ -d "$dir" ]; then
+ mkdir -p "${conn_dir}/packages/connectors/trino-${range}"
+ cp -r "$dir"/* "${conn_dir}/packages/connectors/trino-${range}/"
+ copied=$((copied + 1))
+ else
+ echo "ERROR: expected distribution directory not found: ${dir}" >&2
+ exit 1
+ fi
+done
+
+if [ "${copied}" -eq 0 ]; then
+ echo "ERROR: no Trino connector bands were staged." >&2
+ exit 1
+fi
+
+# Stage the canonical Apache-2.0 LICENSE and NOTICE from the repository root so
+# the image ships the real texts (not drifting copies committed in-tree).
+cp "${gravitino_home}/LICENSE" "${conn_dir}/licenses/LICENSE"
+cp "${gravitino_home}/NOTICE" "${conn_dir}/licenses/NOTICE"
+
+echo ""
+echo "=== Trino connectors prepared (${copied} version range(s)) ==="
+echo "Output: ${conn_dir}/packages/connectors/"
+find "${conn_dir}/packages/connectors/" -maxdepth 1 -type d | sort