diff --git a/ingestion/src/metadata/ingestion/source/dashboard/tableau/client.py b/ingestion/src/metadata/ingestion/source/dashboard/tableau/client.py index 12c6e63eb07..a69823fb981 100644 --- a/ingestion/src/metadata/ingestion/source/dashboard/tableau/client.py +++ b/ingestion/src/metadata/ingestion/source/dashboard/tableau/client.py @@ -13,7 +13,7 @@ Wrapper module of TableauServerConnection client """ import math import traceback -from typing import Callable, Dict, List, Optional, Tuple, Union +from typing import Callable, Dict, Iterable, List, Optional, Tuple, Union import validators from cached_property import cached_property @@ -77,16 +77,18 @@ class TableauClient: self, tableau_server_auth: Union[PersonalAccessTokenAuth, TableauAuth], config, - verify_ssl, + verify_ssl: Union[bool, str], pagination_limit: int, ): self.tableau_server = Server(str(config.hostPort), use_server_version=True) + if config.apiVersion: + self.tableau_server.version = config.apiVersion self.tableau_server.add_http_options({"verify": verify_ssl}) self.tableau_server.auth.sign_in(tableau_server_auth) self.config = config self.pagination_limit = pagination_limit self.custom_sql_table_queries: Dict[str, List[str]] = {} - self.usage_metrics: Dict[str, int] = {} + self.owner_cache: Dict[str, TableauOwner] = {} @cached_property def server_info(self) -> Callable: @@ -101,9 +103,15 @@ class TableauClient: def get_tableau_owner(self, owner_id: str) -> Optional[TableauOwner]: try: + if owner_id in self.owner_cache: + return self.owner_cache[owner_id] owner = self.tableau_server.users.get_by_id(owner_id) if owner_id else None if owner and owner.email: - return TableauOwner(id=owner.id, name=owner.name, email=owner.email) + owner_obj = TableauOwner( + id=owner.id, name=owner.name, email=owner.email + ) + self.owner_cache[owner_id] = owner_obj + return owner_obj except Exception as err: logger.debug(f"Failed to fetch owner details for ID {owner_id}: {str(err)}") return None @@ -130,21 +138,20 @@ class TableauClient: ) view_count += view.total_views except AttributeError as e: - logger.warning( + logger.debug( f"Failed to process view due to missing attribute: {str(e)}" ) continue except Exception as e: - logger.warning(f"Failed to process view: {str(e)}") + logger.debug(f"Failed to process view: {str(e)}") continue return charts, view_count - def get_workbooks(self) -> List[TableauDashboard]: + def get_workbooks(self) -> Iterable[TableauDashboard]: """ Fetch all tableau workbooks """ - workbooks: Optional[List[TableauDashboard]] = [] self.cache_custom_sql_tables() for workbook in Pager(self.tableau_server.workbooks): try: @@ -152,22 +159,20 @@ class TableauClient: charts, user_views = self.get_workbook_charts_and_user_count( workbook.views ) - workbooks.append( - TableauDashboard( - id=workbook.id, - name=workbook.name, - project=TableauBaseModel( - id=workbook.project_id, name=workbook.project_name - ), - description=workbook.description, - owner=self.get_tableau_owner(workbook.owner_id), - tags=workbook.tags, - webpageUrl=workbook.webpage_url, - charts=charts, - dataModels=self.get_datasources(dashboard_id=workbook.id), - user_views=user_views, - ) + workbook = TableauDashboard( + id=workbook.id, + name=workbook.name, + project=TableauBaseModel( + id=workbook.project_id, name=workbook.project_name + ), + owner=self.get_tableau_owner(workbook.owner_id), + description=workbook.description, + tags=workbook.tags, + webpageUrl=workbook.webpage_url, + charts=charts, + user_views=user_views, ) + yield workbook except AttributeError as err: logger.warning( f"Failed to process workbook due to missing attribute: {str(err)}" @@ -176,7 +181,6 @@ class TableauClient: except Exception as err: logger.warning(f"Failed to process workbook: {str(err)}") continue - return workbooks def test_get_workbooks(self): for workbook in Pager(self.tableau_server.workbooks): diff --git a/ingestion/src/metadata/ingestion/source/dashboard/tableau/connection.py b/ingestion/src/metadata/ingestion/source/dashboard/tableau/connection.py index 6d80e697d05..4637bfbd714 100644 --- a/ingestion/src/metadata/ingestion/source/dashboard/tableau/connection.py +++ b/ingestion/src/metadata/ingestion/source/dashboard/tableau/connection.py @@ -13,7 +13,7 @@ Source connection handler """ import traceback -from typing import Any, Dict, Optional +from typing import Any, Dict, Optional, Union import tableauserverclient as TSC @@ -38,7 +38,6 @@ from metadata.ingestion.ometa.ometa_api import OpenMetadata from metadata.ingestion.source.dashboard.tableau.client import TableauClient from metadata.utils.constants import THREE_MIN from metadata.utils.logger import ingestion_logger -from metadata.utils.ssl_registry import get_verify_ssl_fn logger = ingestion_logger() @@ -48,12 +47,12 @@ def get_connection(connection: TableauConnection) -> TableauClient: Create connection """ tableau_server_auth = build_server_config(connection) - get_verify_ssl = get_verify_ssl_fn(connection.verifySSL) + verify_ssl = set_verify_ssl(connection) try: return TableauClient( tableau_server_auth=tableau_server_auth, config=connection, - verify_ssl=get_verify_ssl(connection.sslConfig), + verify_ssl=verify_ssl, pagination_limit=connection.paginationLimit, ) except Exception as exc: @@ -63,6 +62,21 @@ def get_connection(connection: TableauConnection) -> TableauClient: ) +def set_verify_ssl(connection: TableauConnection) -> Union[bool, str]: + """ + Set verify ssl based on connection configuration + ref: https://tableau.github.io/server-client-python/docs/sign-in-out#handling-ssl-certificates-for-tableau-server + """ + if connection.verifySSL.value == "validate": + if connection.sslConfig.root.caCertificate: + return connection.sslConfig.root.caCertificate.get_secret_value() + if connection.sslConfig.root.sslCertificate: + return connection.sslConfig.root.sslCertificate.get_secret_value() + if connection.verifySSL.value == "ignore": + return False + return None + + def test_connection( metadata: OpenMetadata, client: TableauClient, diff --git a/ingestion/src/metadata/ingestion/source/dashboard/tableau/metadata.py b/ingestion/src/metadata/ingestion/source/dashboard/tableau/metadata.py index 8132d9b5ce3..129f129aac8 100644 --- a/ingestion/src/metadata/ingestion/source/dashboard/tableau/metadata.py +++ b/ingestion/src/metadata/ingestion/source/dashboard/tableau/metadata.py @@ -127,8 +127,8 @@ class TableauSource(DashboardServiceSource): ) return cls(config, metadata) - def get_dashboards_list(self) -> Optional[List[TableauDashboard]]: - return self.client.get_workbooks() + def get_dashboards_list(self) -> Iterable[TableauDashboard]: + yield from self.client.get_workbooks() def get_dashboard_name(self, dashboard: TableauDashboard) -> str: return dashboard.name @@ -137,6 +137,7 @@ class TableauSource(DashboardServiceSource): """ Get Dashboard Details including the dashboard charts and datamodels """ + dashboard.dataModels = self.client.get_datasources(dashboard.id) return dashboard def get_owner_ref( diff --git a/openmetadata-spec/src/main/resources/json/schema/entity/services/connections/dashboard/tableauConnection.json b/openmetadata-spec/src/main/resources/json/schema/entity/services/connections/dashboard/tableauConnection.json index 7e0cc43f1de..1206b5e3ab7 100644 --- a/openmetadata-spec/src/main/resources/json/schema/entity/services/connections/dashboard/tableauConnection.json +++ b/openmetadata-spec/src/main/resources/json/schema/entity/services/connections/dashboard/tableauConnection.json @@ -52,6 +52,12 @@ "type": "integer", "default": 10 }, + "apiVersion": { + "title": "API Version", + "description": "Tableau API version. If not provided, the version will be used from the tableau server.", + "type": "string", + "default": null + }, "verifySSL": { "$ref": "../../../../security/ssl/verifySSLConfig.json#/definitions/verifySSL", "default": "no-ssl" diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/entity/services/connections/dashboard/tableauConnection.ts b/openmetadata-ui/src/main/resources/ui/src/generated/entity/services/connections/dashboard/tableauConnection.ts index 9a5cdff48c5..3d68faab2f2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/generated/entity/services/connections/dashboard/tableauConnection.ts +++ b/openmetadata-ui/src/main/resources/ui/src/generated/entity/services/connections/dashboard/tableauConnection.ts @@ -14,6 +14,10 @@ * Tableau Connection Config */ export interface TableauConnection { + /** + * Tableau API version. If not provided, the version will be used from the tableau server. + */ + apiVersion?: string; /** * Types of methods used to authenticate to the tableau instance */