shahar1 commented on code in PR #70160: URL: https://github.com/apache/airflow/pull/70160#discussion_r3646350735
########## providers/oracle/src/airflow/providers/oracle/hooks/base_oci.py: ########## @@ -0,0 +1,233 @@ +# 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. +from __future__ import annotations + +from collections.abc import Callable +from functools import cached_property +from typing import TYPE_CHECKING, Any, Generic, TypeVar + +from airflow.providers.common.compat.sdk import AirflowOptionalProviderFeatureException, BaseHook + +if TYPE_CHECKING: + from airflow.sdk import Connection + + OciSigner = Any + +OciClient = TypeVar("OciClient") + +OCI_AUTH_TYPE_API_KEY = "api_key" +OCI_AUTH_TYPE_CONFIG_FILE = "config_file" +OCI_AUTH_TYPE_INSTANCE_PRINCIPAL = "instance_principal" +OCI_AUTH_TYPE_RESOURCE_PRINCIPAL = "resource_principal" +OCI_AUTH_TYPES = ( + OCI_AUTH_TYPE_API_KEY, + OCI_AUTH_TYPE_CONFIG_FILE, + OCI_AUTH_TYPE_INSTANCE_PRINCIPAL, + OCI_AUTH_TYPE_RESOURCE_PRINCIPAL, +) + + +def _get_oci_sdk() -> Any: + try: + import oci + except ImportError as e: + raise AirflowOptionalProviderFeatureException( + "OCI features require the optional OCI Python SDK. " + "Install it with: pip install 'apache-airflow-providers-oracle[oci]'" + ) from e + return oci + + +class OciBaseHook(BaseHook, Generic[OciClient]): + """ + Base hook for Oracle Cloud Infrastructure services. + + The hook supports API key, OCI configuration file, instance principal, and resource principal + authentication. API key credentials are read from the connection fields, while principal + authentication is delegated to the OCI SDK. + + :param oci_conn_id: The :ref:`OCI connection id <howto/connection:oci>`. + :param auth_type: OCI authentication type selected by the Dag author. + :param key_file: API signing private key path selected by the Dag author. + :param config_file: OCI SDK configuration file selected by the Dag author. + :param profile: Profile to load from the OCI SDK configuration file. + :param service_endpoint: Optional service endpoint selected by the Dag author. + """ + + conn_name_attr = "oci_conn_id" + default_conn_name = "oci_default" + conn_type = "oci" + hook_name = "Oracle Cloud Infrastructure" + client_class: Callable[..., OciClient] | None = None + + def __init__( + self, + oci_conn_id: str = default_conn_name, + *, + auth_type: str = OCI_AUTH_TYPE_API_KEY, + key_file: str | None = None, + config_file: str | None = None, + profile: str | None = None, + service_endpoint: str | None = None, + ) -> None: + super().__init__() + self.oci_conn_id = oci_conn_id + self.auth_type = auth_type + self.key_file = key_file + self.config_file = config_file + self.profile = profile + self.service_endpoint = service_endpoint + + @classmethod + def get_connection_form_widgets(cls) -> dict[str, Any]: + """Return connection widgets to add to the connection form.""" + from flask_appbuilder.fieldwidgets import BS3PasswordFieldWidget, BS3TextFieldWidget + from flask_babel import lazy_gettext + from wtforms import PasswordField, StringField + + return { + "tenancy": StringField(lazy_gettext("Tenancy OCID"), widget=BS3TextFieldWidget()), + "fingerprint": StringField(lazy_gettext("Key Fingerprint"), widget=BS3TextFieldWidget()), + "key_content": PasswordField( + lazy_gettext("Private Key Content"), widget=BS3PasswordFieldWidget() + ), Review Comment: `key_content` extra holds the raw API signing key but matches no entry in `DEFAULT_SENSITIVE_FIELDS`, so it is not redacted by `GET /api/v2/connections/{id}` or `mask_secret`. format: password only masks the input widget. If you rename to `private_key_content` it gets auto-masked. ########## providers/oracle/src/airflow/providers/oracle/hooks/base_oci.py: ########## @@ -0,0 +1,233 @@ +# 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. +from __future__ import annotations + +from collections.abc import Callable +from functools import cached_property +from typing import TYPE_CHECKING, Any, Generic, TypeVar + +from airflow.providers.common.compat.sdk import AirflowOptionalProviderFeatureException, BaseHook + +if TYPE_CHECKING: + from airflow.sdk import Connection + + OciSigner = Any + +OciClient = TypeVar("OciClient") + +OCI_AUTH_TYPE_API_KEY = "api_key" +OCI_AUTH_TYPE_CONFIG_FILE = "config_file" +OCI_AUTH_TYPE_INSTANCE_PRINCIPAL = "instance_principal" +OCI_AUTH_TYPE_RESOURCE_PRINCIPAL = "resource_principal" +OCI_AUTH_TYPES = ( + OCI_AUTH_TYPE_API_KEY, + OCI_AUTH_TYPE_CONFIG_FILE, + OCI_AUTH_TYPE_INSTANCE_PRINCIPAL, + OCI_AUTH_TYPE_RESOURCE_PRINCIPAL, +) + + +def _get_oci_sdk() -> Any: + try: + import oci + except ImportError as e: + raise AirflowOptionalProviderFeatureException( + "OCI features require the optional OCI Python SDK. " + "Install it with: pip install 'apache-airflow-providers-oracle[oci]'" + ) from e + return oci + + +class OciBaseHook(BaseHook, Generic[OciClient]): + """ + Base hook for Oracle Cloud Infrastructure services. + + The hook supports API key, OCI configuration file, instance principal, and resource principal + authentication. API key credentials are read from the connection fields, while principal + authentication is delegated to the OCI SDK. + + :param oci_conn_id: The :ref:`OCI connection id <howto/connection:oci>`. + :param auth_type: OCI authentication type selected by the Dag author. + :param key_file: API signing private key path selected by the Dag author. + :param config_file: OCI SDK configuration file selected by the Dag author. + :param profile: Profile to load from the OCI SDK configuration file. + :param service_endpoint: Optional service endpoint selected by the Dag author. + """ + + conn_name_attr = "oci_conn_id" + default_conn_name = "oci_default" + conn_type = "oci" + hook_name = "Oracle Cloud Infrastructure" + client_class: Callable[..., OciClient] | None = None Review Comment: `client_class` is unreachable by construction (any OCI client class needs the optional SDK at import time), so subclasses must override `_get_client_class()` instead. Two mechanisms, one usable. ########## providers/oracle/src/airflow/providers/oracle/hooks/base_oci.py: ########## @@ -0,0 +1,233 @@ +# 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. +from __future__ import annotations + +from collections.abc import Callable +from functools import cached_property +from typing import TYPE_CHECKING, Any, Generic, TypeVar + +from airflow.providers.common.compat.sdk import AirflowOptionalProviderFeatureException, BaseHook + +if TYPE_CHECKING: + from airflow.sdk import Connection + + OciSigner = Any + +OciClient = TypeVar("OciClient") + +OCI_AUTH_TYPE_API_KEY = "api_key" +OCI_AUTH_TYPE_CONFIG_FILE = "config_file" +OCI_AUTH_TYPE_INSTANCE_PRINCIPAL = "instance_principal" +OCI_AUTH_TYPE_RESOURCE_PRINCIPAL = "resource_principal" +OCI_AUTH_TYPES = ( + OCI_AUTH_TYPE_API_KEY, + OCI_AUTH_TYPE_CONFIG_FILE, + OCI_AUTH_TYPE_INSTANCE_PRINCIPAL, + OCI_AUTH_TYPE_RESOURCE_PRINCIPAL, +) + + +def _get_oci_sdk() -> Any: + try: + import oci + except ImportError as e: + raise AirflowOptionalProviderFeatureException( + "OCI features require the optional OCI Python SDK. " + "Install it with: pip install 'apache-airflow-providers-oracle[oci]'" + ) from e + return oci + + +class OciBaseHook(BaseHook, Generic[OciClient]): + """ + Base hook for Oracle Cloud Infrastructure services. + + The hook supports API key, OCI configuration file, instance principal, and resource principal + authentication. API key credentials are read from the connection fields, while principal + authentication is delegated to the OCI SDK. + + :param oci_conn_id: The :ref:`OCI connection id <howto/connection:oci>`. + :param auth_type: OCI authentication type selected by the Dag author. + :param key_file: API signing private key path selected by the Dag author. + :param config_file: OCI SDK configuration file selected by the Dag author. + :param profile: Profile to load from the OCI SDK configuration file. + :param service_endpoint: Optional service endpoint selected by the Dag author. + """ + + conn_name_attr = "oci_conn_id" + default_conn_name = "oci_default" + conn_type = "oci" + hook_name = "Oracle Cloud Infrastructure" + client_class: Callable[..., OciClient] | None = None + + def __init__( + self, + oci_conn_id: str = default_conn_name, + *, + auth_type: str = OCI_AUTH_TYPE_API_KEY, + key_file: str | None = None, + config_file: str | None = None, + profile: str | None = None, + service_endpoint: str | None = None, + ) -> None: + super().__init__() + self.oci_conn_id = oci_conn_id + self.auth_type = auth_type + self.key_file = key_file + self.config_file = config_file + self.profile = profile + self.service_endpoint = service_endpoint + + @classmethod + def get_connection_form_widgets(cls) -> dict[str, Any]: + """Return connection widgets to add to the connection form.""" + from flask_appbuilder.fieldwidgets import BS3PasswordFieldWidget, BS3TextFieldWidget + from flask_babel import lazy_gettext + from wtforms import PasswordField, StringField + + return { + "tenancy": StringField(lazy_gettext("Tenancy OCID"), widget=BS3TextFieldWidget()), + "fingerprint": StringField(lazy_gettext("Key Fingerprint"), widget=BS3TextFieldWidget()), + "key_content": PasswordField( + lazy_gettext("Private Key Content"), widget=BS3PasswordFieldWidget() + ), + "region": StringField(lazy_gettext("Region"), widget=BS3TextFieldWidget()), + "compartment_id": StringField(lazy_gettext("Compartment OCID"), widget=BS3TextFieldWidget()), + } + + @classmethod + def get_ui_field_behaviour(cls) -> dict[str, Any]: + """Return custom field behavior for the connection form.""" + return { + "hidden_fields": ["host", "schema", "port"], + "relabeling": { + "login": "User OCID", + "password": "Private Key Passphrase", + }, + "placeholders": { + "login": "ocid1.user...", + "password": "Optional API key passphrase", + "tenancy": "ocid1.tenancy...", + "fingerprint": "aa:bb:cc:...", + "region": "us-chicago-1", + "compartment_id": "ocid1.compartment...", + }, + } + + @cached_property + def connection(self) -> Connection: + """Return the configured Airflow connection.""" + return self.get_connection(self.oci_conn_id) + + def get_oci_config(self) -> tuple[dict[str, Any], OciSigner | None]: + """Build OCI SDK configuration and an optional signer from the Airflow connection.""" + oci = _get_oci_sdk() + conn = self.connection Review Comment: `self.connection` read unconditionally, so `instance_principal`/`resource_principal` still require an `oci_default` row to exist. `AwsBaseHook.conn_config` catches this and carries on. ########## providers/oracle/provider.yaml: ########## @@ -91,6 +92,10 @@ integrations: - /docs/apache-airflow-providers-oracle/operators.rst logo: /docs/integration-logos/Oracle.png tags: [software] + - integration-name: Oracle Cloud Infrastructure Review Comment: nit: New integration has no `how-to-guide`, so `generative_ai.rst` isn't linked from the integrations index. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
