This is an automated email from the ASF dual-hosted git repository.

yuqi1129 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new 2f93e96de1 [#13265] feat(connector): Add UBI-based Docker images for 
Trino, Flink and Spark connectors (#13266)
2f93e96de1 is described below

commit 2f93e96de125af7d981d7725b67c3bdf0ab84d2d
Author: geyanggang <[email protected]>
AuthorDate: Thu Sep 17 21:07:44 2026 +0800

    [#13265] feat(connector): Add UBI-based Docker images for Trino, Flink and 
Spark connectors (#13266)
    
    ### What changes were proposed in this pull request?
    
    Add in-tree Docker build definitions for the three engine connectors
    under
    `dev/docker/`:
    
    - `dev/docker/trino-connectors/`
    - `dev/docker/flink-connectors/`
    - `dev/docker/spark-connectors/`
    
    Each directory contains:
    
    - `Dockerfile` — Red Hat UBI 10 minimal base, required certification
    labels
      (`name`, `vendor`, `version`, `release`, `summary`, `description`,
    `maintainer`), `/licenses` with `LICENSE` and `NOTICE`, runs as non-root
      (UID 1000, GID 0), OpenShift arbitrary-UID compatible.
    - `*-connectors-dependency.sh` — builds the connector artifacts. The set
    of
      version modules is discovered from the Gradle project rather than
    hard-coded, so the same script tracks whatever versions the branch
    supports:
    - Trino: each `trino-connector-<band>` module via
    `assembleTrinoConnector`.
      - Flink: each `flink-runtime-<ver>` module (Scala 2.12).
      - Spark: each `spark-runtime-<major>` module, deriving Scala variants
    (2.12 always; 2.13 for Spark 3.4+; Spark 4.0 pinned to 2.13, detected
        from its build script).
    - `copy-connector.sh` — entrypoint. Resolves the requested version from
    the
      directories baked into the image at runtime and copies the jars into a
    mounted `/target` volume; prints available versions when `/target` is
    not
      mounted.
    - `README.md` — install and configuration guide.
    
    The Trino image ships the full plugin layout (connector jar plus its
    runtime
    dependency jars, `LICENSE`, `NOTICE`, `README`) and no database JDBC
    drivers;
    those come from Trino's own `mysql` / `postgresql` plugins. Flink and
    Spark
    images ship the shaded runtime jars.
    
    ### Why are the changes needed?
    
    There is no first-party connector container image in the repository for
    installing the connectors into Trino, Flink or Spark on Kubernetes.
    These
    images give operators a ready-to-use init container per engine, with
    contents
    that stay purely Apache-2.0 and a version matrix that follows the branch
    automatically.
    
    Fix: #13265
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. It adds new connector container images
    (`apache/gravitino-trino-connector`,
    `apache/gravitino-flink-connector`, `apache/gravitino-spark-connector`)
    and
    their build tooling. No API or property changes.
    
    ### How was this patch tested?
    
    - Built the Flink connector image end to end: the dependency script
    discovered
    and built all Flink runtime modules (1.18/1.19/1.20), the image built on
    UBI 10,
    and the entrypoint copied the correct jar for the requested
    `FLINK_VERSION`
      into a mounted `/target`.
    - Verified the produced jars contain only open source connector code (no
    proprietary JDBC adapters) and that `/licenses` contains only `LICENSE`
    and
      `NOTICE`.
    - Verified the required image labels and non-root user (UID 1000) are
    present.
    - Confirmed the Trino and Spark dependency scripts discover the correct
    Gradle
    modules and construct valid task lists (including the Spark
    Scala-variant
    derivation for 3.5 / 4.0), and validated all shell scripts with `bash
    -n`.
---
 .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

Reply via email to