Skip to content
Open
Show file tree
Hide file tree
Changes from 24 commits
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
187 changes: 171 additions & 16 deletions ingestion/src/metadata/ingestion/source/database/oracle/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,18 @@
Source connection handler
"""

import base64
import binascii
import io
import os
import shutil
import sys
import tempfile
import weakref
import zipfile
from copy import deepcopy
from typing import Optional
from pathlib import Path
from typing import Any, Optional
from urllib.parse import quote_plus

import oracledb
Expand All @@ -28,20 +36,22 @@
Workflow as AutomationWorkflow,
)
from metadata.generated.schema.entity.services.connections.database.oracleConnection import (
OracleConnection as OracleConnectionConfig,
)
from metadata.generated.schema.entity.services.connections.database.oracleConnection import (
OracleAutonomousConnection,
OracleDatabaseSchema,
OracleServiceName,
OracleTNSConnection,
)
from metadata.generated.schema.entity.services.connections.database.oracleConnection import (
OracleConnection as OracleConnectionConfig,
)
from metadata.generated.schema.entity.services.connections.testConnectionResult import (
TestConnectionResult,
)
from metadata.ingestion.connections.builders import (
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 @@ -68,24 +78,161 @@
class OracleConnection(BaseConnection[OracleConnectionConfig, Engine]):
def __init__(self, connection: OracleConnectionConfig):
super().__init__(connection)
self._wallet_temp_dir: str | None = None
Comment thread
hassaansaleem28 marked this conversation as resolved.
Comment thread
hassaansaleem28 marked this conversation as resolved.
self._wallet_cleanup_finalizer: Any = None

def _set_wallet_temp_dir(self, wallet_temp_dir: str) -> None:
self._cleanup_wallet_temp_dir()
self._wallet_temp_dir = wallet_temp_dir
self._wallet_cleanup_finalizer = weakref.finalize(
self,
shutil.rmtree,
wallet_temp_dir,
ignore_errors=True,
)

def _cleanup_wallet_temp_dir(self) -> None:
wallet_temp_dir = self._wallet_temp_dir
if self._wallet_cleanup_finalizer and self._wallet_cleanup_finalizer.alive:
self._wallet_cleanup_finalizer()
elif wallet_temp_dir:
shutil.rmtree(wallet_temp_dir, ignore_errors=True)

self._wallet_cleanup_finalizer = None
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,
) -> OracleAutonomousConnection:
return connection_type

@staticmethod
def _safe_extract_wallet_archive(zip_ref: zipfile.ZipFile, target_dir: str) -> None:
target_root = Path(target_dir).resolve()

for member in zip_ref.infolist():
member_path = (target_root / member.filename).resolve()

if member_path != target_root and target_root not in member_path.parents:
raise ValueError("Invalid walletContent. Wallet zip contains unsafe file paths.")

if member.is_dir():
OracleConnection._mkdir_secure_within(member_path, target_root)
continue

OracleConnection._mkdir_secure_within(member_path.parent, target_root)
with (
zip_ref.open(member, "r") as source_file,
open(
member_path,
"wb",
opener=lambda path, flags: os.open(path, flags, 0o600),
) as target_file,
):
shutil.copyfileobj(source_file, target_file)

@staticmethod
def _mkdir_secure_within(path: Path, root: Path) -> None:
"""Create path and any intermediate dirs with 0o700, only within root."""
if path == root:
return
OracleConnection._mkdir_secure_within(path.parent, root)
try:
path.mkdir(mode=0o700, exist_ok=False)
except FileExistsError:
return
Comment thread
gitar-bot[bot] marked this conversation as resolved.
Outdated
Comment thread
hassaansaleem28 marked this conversation as resolved.
Outdated
path.chmod(0o700)

Comment thread
hassaansaleem28 marked this conversation as resolved.
Comment thread
hassaansaleem28 marked this conversation as resolved.
Outdated
def _extract_wallet_content(self, wallet_content: SecretStr) -> str:
# Strip whitespace/newlines so wrapped base64 (e.g. from `base64 -i` on macOS,
# which inserts line breaks every 76 chars) decodes the same as a single line.
sanitized = "".join(wallet_content.get_secret_value().split())
try:
decoded_wallet = base64.b64decode(sanitized, validate=True)
except (binascii.Error, TypeError) as exc:
raise ValueError("Invalid walletContent. Expected a base64-encoded wallet zip.") from exc

wallet_temp_dir = tempfile.mkdtemp(prefix="oracle_wallet_")
self._set_wallet_temp_dir(wallet_temp_dir)

try:
with zipfile.ZipFile(io.BytesIO(decoded_wallet)) as zip_ref:
self._safe_extract_wallet_archive(zip_ref, 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
except Exception:
self._cleanup_wallet_temp_dir()
raise

return wallet_temp_dir

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)
if not self.service_connection.connectionArguments:
self.service_connection.connectionArguments = init_empty_connection_arguments()
if self.service_connection.connectionArguments.root is None:
self.service_connection.connectionArguments.root = {}

connection_arguments: dict[str, Any] = self.service_connection.connectionArguments.root

wallet_path = autonomous_connection.walletPath
if autonomous_connection.walletContent:
if self._wallet_temp_dir and Path(self._wallet_temp_dir).is_dir():
wallet_path = self._wallet_temp_dir
else:
wallet_path = self._extract_wallet_content(autonomous_connection.walletContent)
else:
self._cleanup_wallet_temp_dir()

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

connection_arguments["config_dir"] = wallet_path
connection_arguments["wallet_location"] = wallet_path

if autonomous_connection.walletPassword:
connection_arguments["wallet_password"] = autonomous_connection.walletPassword.get_secret_value()
else:
connection_arguments.pop("wallet_password", None)

def _uses_inline_wallet_content(self) -> bool:
connection_type = self.service_connection.oracleConnectionType
return bool(isinstance(connection_type, OracleAutonomousConnection) and connection_type.walletContent)

def _get_client(self) -> Engine:
"""
Create connection
"""
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.
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}")

return create_generic_db_connection(
connection=self.service_connection,
get_connection_url_fn=self.get_connection_url,
get_connection_args_fn=get_connection_args_common,
)
return create_generic_db_connection(
connection=self.service_connection,
get_connection_url_fn=self.get_connection_url,
get_connection_args_fn=get_connection_args_common,
)
except Exception:
if self._uses_inline_wallet_content():
self._cleanup_wallet_temp_dir()
raise

def test_connection(
self,
Expand Down Expand Up @@ -139,6 +286,9 @@ def get_connection_dict(self) -> dict:
connection_dict["database"] = connection_copy.oracleConnectionType.oracleServiceName
elif isinstance(connection_copy.oracleConnectionType, OracleTNSConnection):
connection_dict["host"] = connection_copy.oracleConnectionType.oracleTNSConnection
Comment thread
hassaansaleem28 marked this conversation as resolved.
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 @@ -191,6 +341,11 @@ 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
Loading
Loading