Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
08980a0
feat(ingestion): add OCI Autonomous Database support for Oracle conne…
hassaansaleem28 Apr 18, 2026
e170023
fix bot suggestions
hassaansaleem28 Apr 18, 2026
623f99e
Update ingestion/src/metadata/ingestion/source/database/oracle/connec…
hassaansaleem28 Apr 18, 2026
2c6c311
Update ingestion/src/metadata/ingestion/source/database/oracle/connec…
hassaansaleem28 Apr 18, 2026
4f23116
Update ingestion/src/metadata/ingestion/source/database/oracle/connec…
hassaansaleem28 Apr 19, 2026
9deb3fd
Merge branch 'main' into issue-27443-oci-autonomous
hassaansaleem28 Apr 19, 2026
d426b5d
fix copilot suggestions
hassaansaleem28 Apr 19, 2026
998d4fd
fix(oracle): cleanup inline wallet temp dir on connect failure
hassaansaleem28 Apr 19, 2026
9c0a3f0
Merge branch 'main' into issue-27443-oci-autonomous
hassaansaleem28 Apr 24, 2026
ba98a45
docs(oracle): clarify walletContent base64 encoding for Docker/K8s
hassaansaleem28 Apr 25, 2026
43e1b80
Merge branch 'main' into issue-27443-oci-autonomous
hassaansaleem28 Apr 25, 2026
e991b85
Merge branch 'main' into issue-27443-oci-autonomous
hassaansaleem28 Apr 28, 2026
6453951
Merge branch 'main' into issue-27443-oci-autonomous
hassaansaleem28 Apr 28, 2026
d76b997
fix(oracle): address review feedback on autonomous connection
hassaansaleem28 Apr 28, 2026
e06b07d
fix(oracle): drop anyOf from autonomous schema to keep generated mode…
hassaansaleem28 Apr 29, 2026
f0c823e
Merge branch 'main' into issue-27443-oci-autonomous
hassaansaleem28 Apr 29, 2026
12b4169
Update ingestion/src/metadata/ingestion/source/database/oracle/connec…
hassaansaleem28 Apr 29, 2026
e4e67c5
Update openmetadata-spec/src/main/resources/json/schema/entity/servic…
hassaansaleem28 Apr 29, 2026
7cac5f3
Update openmetadata-ui/src/main/resources/ui/public/locales/en-US/Dat…
hassaansaleem28 Apr 29, 2026
6e3c514
Update openmetadata-spec/src/main/resources/json/schema/entity/servic…
hassaansaleem28 Apr 29, 2026
a1ad81f
fix(oracle): satisfy basedpyright on connection_arguments narrowing
hassaansaleem28 Apr 29, 2026
3e97044
fix(oracle): create wallet subdirectories with 0o700 permissions
hassaansaleem28 Apr 29, 2026
d292bca
fix(oracle): tolerate wrapped base64 in walletContent
hassaansaleem28 Apr 29, 2026
ecbfcc8
Merge branch 'main' into issue-27443-oci-autonomous
hassaansaleem28 Apr 29, 2026
ff74d1a
fix(oracle): guard _mkdir_secure_within against unbounded recursion
hassaansaleem28 Apr 29, 2026
a3a4bea
Merge branch 'main' into issue-27443-oci-autonomous
hassaansaleem28 Apr 29, 2026
62594a4
refactor(oracle): remove pass-through _get_autonomous_connection_config
hassaansaleem28 Apr 30, 2026
cd93ec2
Merge branch 'main' into issue-27443-oci-autonomous
hassaansaleem28 May 4, 2026
ba9d067
Potential fix for pull request finding
hassaansaleem28 May 4, 2026
73d5ea6
Update generated TypeScript types
hassaansaleem28 May 4, 2026
dfa1f97
Update generated TypeScript types
hassaansaleem28 May 4, 2026
a50af0a
Potential fix for pull request finding
hassaansaleem28 May 4, 2026
d2a58c4
fix(oracle): restore X | None over Optional[X] for ruff UP045
hassaansaleem28 May 4, 2026
ee91fb9
Merge branch 'main' into issue-27443-oci-autonomous
hassaansaleem28 May 4, 2026
6ecf6c3
Merge branch 'main' into issue-27443-oci-autonomous
hassaansaleem28 May 5, 2026
cc88740
Merge branch 'main' into issue-27443-oci-autonomous
hassaansaleem28 May 6, 2026
4ecc9a4
Potential fix for pull request finding
hassaansaleem28 May 6, 2026
90f9879
Potential fix for pull request finding
hassaansaleem28 May 6, 2026
1d1f94e
Potential fix for pull request finding
hassaansaleem28 May 6, 2026
473c89d
fix(oracle): switch _wallet_temp_dir back to X | None for ruff UP045
hassaansaleem28 May 6, 2026
4aa5095
Merge branch 'main' into issue-27443-oci-autonomous
hassaansaleem28 May 6, 2026
5993897
Merge branch 'main' into issue-27443-oci-autonomous
hassaansaleem28 May 6, 2026
cc9b365
Merge branch 'main' into issue-27443-oci-autonomous
hassaansaleem28 May 7, 2026
8eedea1
Merge branch 'main' into issue-27443-oci-autonomous
hassaansaleem28 May 7, 2026
2249cd7
Merge branch 'main' into issue-27443-oci-autonomous
hassaansaleem28 May 7, 2026
8067108
Merge branch 'main' into issue-27443-oci-autonomous
hassaansaleem28 May 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 115 additions & 12 deletions ingestion/src/metadata/ingestion/source/database/oracle/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,15 @@
"""
Source connection handler
"""
import base64
import io
import os
import shutil
import sys
import tempfile
import zipfile
from copy import deepcopy
from typing import Optional
from typing import Any, Optional
from urllib.parse import quote_plus

import oracledb
Expand All @@ -30,6 +35,7 @@
OracleConnection as OracleConnectionConfig,
)
from metadata.generated.schema.entity.services.connections.database.oracleConnection import (
OracleAutonomousConnection,
OracleDatabaseSchema,
OracleServiceName,
OracleTNSConnection,
Expand All @@ -41,6 +47,7 @@
create_generic_db_connection,
get_connection_args_common,
get_connection_options_dict,
init_empty_connection_arguments,
)
from metadata.ingestion.connections.connection import BaseConnection
from metadata.ingestion.connections.secrets import connection_with_options_secrets
Expand All @@ -67,22 +74,104 @@
class OracleConnection(BaseConnection[OracleConnectionConfig, Engine]):
def __init__(self, connection: OracleConnectionConfig):
super().__init__(connection)
self._wallet_temp_dir: Optional[str] = None

def __del__(self):
self._cleanup_wallet_temp_dir()
Comment thread
gitar-bot[bot] marked this conversation as resolved.
Outdated

def _cleanup_wallet_temp_dir(self) -> None:
if self._wallet_temp_dir:
shutil.rmtree(self._wallet_temp_dir, ignore_errors=True)
self._wallet_temp_dir = None

def _is_autonomous_connection(self) -> bool:
return isinstance(
self.service_connection.oracleConnectionType, OracleAutonomousConnection
)

@staticmethod
def _get_autonomous_connection_config(
connection_type: OracleAutonomousConnection,
) -> Any:
return connection_type.root
Comment thread
hassaansaleem28 marked this conversation as resolved.
Outdated

Comment thread
hassaansaleem28 marked this conversation as resolved.
def _extract_wallet_content(self, wallet_content: SecretStr) -> str:
try:
decoded_wallet = base64.b64decode(wallet_content.get_secret_value())
except (ValueError, TypeError) as exc:
raise ValueError(
"Invalid walletContent. Expected a base64-encoded wallet zip."
) from exc
Comment thread
hassaansaleem28 marked this conversation as resolved.
Outdated
Comment thread
hassaansaleem28 marked this conversation as resolved.
Outdated

self._cleanup_wallet_temp_dir()
self._wallet_temp_dir = tempfile.mkdtemp(prefix="oracle_wallet_")

try:
with zipfile.ZipFile(io.BytesIO(decoded_wallet)) as zip_ref:
zip_ref.extractall(self._wallet_temp_dir)
except zipfile.BadZipFile as exc:
Comment thread
hassaansaleem28 marked this conversation as resolved.
self._cleanup_wallet_temp_dir()
raise ValueError(
"Invalid walletContent. Expected a valid zip archive."
) from exc

return self._wallet_temp_dir
Comment thread
gitar-bot[bot] marked this conversation as resolved.
Outdated

def _configure_autonomous_connection_arguments(self) -> None:
connection_type = self.service_connection.oracleConnectionType
if not isinstance(connection_type, OracleAutonomousConnection):
return

autonomous_connection = self._get_autonomous_connection_config(connection_type)

wallet_path = autonomous_connection.walletPath
if autonomous_connection.walletContent:
wallet_path = self._extract_wallet_content(
autonomous_connection.walletContent
)

if not wallet_path:
raise ValueError(
"Oracle Autonomous connections require either walletPath or walletContent."
)

if not self.service_connection.connectionArguments:
self.service_connection.connectionArguments = (
init_empty_connection_arguments()
)
elif self.service_connection.connectionArguments.root is None:
self.service_connection.connectionArguments.root = {}

self.service_connection.connectionArguments.root["config_dir"] = wallet_path
self.service_connection.connectionArguments.root[
"wallet_location"
] = wallet_path

if autonomous_connection.walletPassword:
self.service_connection.connectionArguments.root[
"wallet_password"
] = autonomous_connection.walletPassword.get_secret_value()

def _get_client(self) -> Engine:
"""
Create connection
"""
try:
if self.service_connection.instantClientDirectory:
logger.info(
f"Initializing Oracle thick client at {self.service_connection.instantClientDirectory}"
)
os.environ[LD_LIB_ENV] = self.service_connection.instantClientDirectory
oracledb.init_oracle_client(
lib_dir=self.service_connection.instantClientDirectory
)
except DatabaseError as err:
logger.info(f"Could not initialize Oracle thick client: {err}")
self._configure_autonomous_connection_arguments()
Comment thread
gitar-bot[bot] marked this conversation as resolved.

if not self._is_autonomous_connection():
try:
if self.service_connection.instantClientDirectory:
logger.info(
f"Initializing Oracle thick client at {self.service_connection.instantClientDirectory}"
)
os.environ[
LD_LIB_ENV
] = self.service_connection.instantClientDirectory
oracledb.init_oracle_client(
lib_dir=self.service_connection.instantClientDirectory
)
except DatabaseError as err:
logger.info(f"Could not initialize Oracle thick client: {err}")

Comment thread
hassaansaleem28 marked this conversation as resolved.
Comment thread
hassaansaleem28 marked this conversation as resolved.
return create_generic_db_connection(
connection=self.service_connection,
Expand Down Expand Up @@ -150,6 +239,13 @@ def get_connection_dict(self) -> dict:
connection_dict[
"host"
] = connection_copy.oracleConnectionType.oracleTNSConnection
elif isinstance(
connection_copy.oracleConnectionType, OracleAutonomousConnection
):
autonomous_connection = self._get_autonomous_connection_config(
connection_copy.oracleConnectionType
)
connection_dict["host"] = autonomous_connection.tnsAlias

# Add connection options if present
if connection_copy.connectionOptions and connection_copy.connectionOptions.root:
Expand Down Expand Up @@ -209,6 +305,13 @@ def _handle_connection_type(url: str, connection: OracleConnectionConfig) -> str
url += connection.oracleConnectionType.oracleTNSConnection
return url

if isinstance(connection.oracleConnectionType, OracleAutonomousConnection):
autonomous_connection = OracleConnection._get_autonomous_connection_config(
connection.oracleConnectionType
)
url += autonomous_connection.tnsAlias
return url

# If not TNS, we add the hostPort
url += connection.hostPort

Expand Down
109 changes: 109 additions & 0 deletions ingestion/tests/unit/test_source_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import base64
import io
import os
import zipfile
from unittest import TestCase
from unittest.mock import patch

from trino.auth import BasicAuthentication, JWTAuthentication, OAuth2Authentication

Expand Down Expand Up @@ -78,6 +83,7 @@
OracleConnection as OracleConnectionConfig,
)
from metadata.generated.schema.entity.services.connections.database.oracleConnection import (
OracleAutonomousConnection,
OracleDatabaseSchema,
OracleScheme,
OracleServiceName,
Expand Down Expand Up @@ -1227,6 +1233,109 @@ def test_oracle_url(self):
)
assert OracleConnection.get_connection_url(oracle_conn_obj) == expected_url

expected_url = "oracle+cx_oracle://admin:password@myadb_high"
oracle_conn_obj = OracleConnectionConfig(
username="admin",
password="password",
oracleConnectionType=OracleAutonomousConnection(
tnsAlias="myadb_high",
walletPath="/tmp/my_wallet",
),
)
assert OracleConnection.get_connection_url(oracle_conn_obj) == expected_url

expected_url = [
"oracle+cx_oracle://admin:password@myadb_high?test_key_2=test_value_2&test_key_1=test_value_1",
"oracle+cx_oracle://admin:password@myadb_high?test_key_1=test_value_1&test_key_2=test_value_2",
]
oracle_conn_obj = OracleConnectionConfig(
username="admin",
password="password",
oracleConnectionType=OracleAutonomousConnection(
tnsAlias="myadb_high",
walletPath="/tmp/my_wallet",
),
connectionOptions=dict(
test_key_1="test_value_1", test_key_2="test_value_2"
),
)
assert OracleConnection.get_connection_url(oracle_conn_obj) in expected_url

@patch(
"metadata.ingestion.source.database.oracle.connection.oracledb.init_oracle_client"
)
@patch(
"metadata.ingestion.source.database.oracle.connection.create_generic_db_connection"
)
def test_oracle_autonomous_wallet_path_args(
self, mock_create_generic_db_connection, mock_init_oracle_client
):
connection = OracleConnectionConfig(
username="admin",
password="password",
instantClientDirectory="/instantclient",
oracleConnectionType=OracleAutonomousConnection(
tnsAlias="myadb_high",
walletPath="/tmp/my_wallet",
walletPassword="wallet_password",
),
)
oracle_connection = OracleConnection(connection)
mock_create_generic_db_connection.return_value = "dummy_engine"

oracle_connection._get_client()

assert mock_init_oracle_client.call_count == 0
assert (
oracle_connection.service_connection.connectionArguments.root["config_dir"]
== "/tmp/my_wallet"
)
assert (
oracle_connection.service_connection.connectionArguments.root[
"wallet_location"
]
== "/tmp/my_wallet"
)
assert (
oracle_connection.service_connection.connectionArguments.root[
"wallet_password"
]
== "wallet_password"
)

@patch(
"metadata.ingestion.source.database.oracle.connection.create_generic_db_connection"
)
def test_oracle_autonomous_wallet_content_args(
self, mock_create_generic_db_connection
):
wallet_bytes = io.BytesIO()
with zipfile.ZipFile(wallet_bytes, "w", zipfile.ZIP_DEFLATED) as zip_file:
zip_file.writestr("tnsnames.ora", "MYADB_HIGH=(DESCRIPTION=...)")

encoded_wallet = base64.b64encode(wallet_bytes.getvalue()).decode("utf-8")

connection = OracleConnectionConfig(
username="admin",
password="password",
oracleConnectionType=OracleAutonomousConnection(
tnsAlias="myadb_high",
walletContent=encoded_wallet,
),
)
oracle_connection = OracleConnection(connection)
mock_create_generic_db_connection.return_value = "dummy_engine"

oracle_connection._get_client()

wallet_dir = oracle_connection.service_connection.connectionArguments.root[
"config_dir"
]
assert os.path.isdir(wallet_dir)
assert os.path.exists(os.path.join(wallet_dir, "tnsnames.ora"))
oracle_connection._cleanup_wallet_temp_dir()
assert not os.path.exists(wallet_dir)

def test_exasol_url(self):
from metadata.ingestion.source.database.exasol.connection import (
get_connection_url,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,49 @@
"required": [
"oracleTNSConnection"
]
},
"OracleAutonomousConnection": {
"title": "Oracle Autonomous Connection",
"type": "object",
"properties": {
"tnsAlias": {
"title": "TNS Alias",
"description": "Service alias defined in the wallet tnsnames.ora file, such as myadb_high.",
"type": "string"
},
"walletPath": {
"title": "Wallet Path",
"description": "Path to the extracted Oracle wallet directory on the ingestion host.",
"type": "string"
},
"walletContent": {
"title": "Wallet Content",
"description": "Base64-encoded Oracle wallet zip content. If provided, OpenMetadata extracts it at runtime.",
"type": "string",
"format": "password"
},
Comment thread
hassaansaleem28 marked this conversation as resolved.
"walletPassword": {
"title": "Wallet Password",
"description": "Wallet password for Oracle Autonomous mTLS connections, if required.",
"type": "string",
"format": "password"
}
},
"required": [
"tnsAlias"
Comment thread
hassaansaleem28 marked this conversation as resolved.
],
"anyOf": [
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OracleAutonomousConnection schema only requires tnsAlias, but the ingestion runtime errors if neither walletPath nor walletContent is provided. Add JSON Schema validation (e.g., anyOf/oneOf with required: [walletPath] or required: [walletContent]) so UI/API config is rejected early instead of failing at runtime.

Copilot uses AI. Check for mistakes.
{
"required": [
"walletPath"
]
},
{
"required": [
"walletContent"
]
}
]
}
Comment thread
hassaansaleem28 marked this conversation as resolved.
},
"properties": {
Expand Down Expand Up @@ -97,7 +140,7 @@
"oracleConnectionType": {
"title": "Oracle Connection Type",
"type": "object",
"description": "Connect with oracle by either passing service name or database schema name.",
"description": "Connect with Oracle by using schema, service name, TNS connection string, or Oracle Autonomous wallet configuration.",
"oneOf": [
{
"$ref": "#/definitions/OracleDatabaseSchema"
Expand All @@ -107,6 +150,9 @@
},
{
"$ref": "#/definitions/OracleTNSConnection"
},
{
"$ref": "#/definitions/OracleAutonomousConnection"
}
]
Comment thread
hassaansaleem28 marked this conversation as resolved.
Comment thread
hassaansaleem28 marked this conversation as resolved.
},
Expand Down
Loading
Loading