From b791ad5c9240168864351da39b29c6e565d40c58 Mon Sep 17 00:00:00 2001 From: Eric Maynard Date: Wed, 18 Jun 2025 15:23:40 -0700 Subject: [PATCH 01/11] add arguments and hints --- client/python/cli/constants.py | 56 ++++++++++++++++++++++++ client/python/cli/options/option_tree.py | 26 +++++++++-- 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/client/python/cli/constants.py b/client/python/cli/constants.py index c9f28ba3c3..4c57268d4b 100644 --- a/client/python/cli/constants.py +++ b/client/python/cli/constants.py @@ -48,6 +48,25 @@ class PrincipalType(Enum): SERVICE = 'service' +class ConnectionType(Enum): + """ + Represents a ConnectionType for an EXTERNAL catalog -- see ConnectionConfigInfo in the spec + """ + + HADOOP = 'hadoop' + HIVE = 'hive' + + +class AuthenticationType(Enum): + """ + Represents a AuthenticationType for an EXTERNAL catalog -- see AuthenticationParameters in the spec + """ + + OAUTH = 'oauth' + BEARER = 'bearer' + SIGV4 = 'sigv4' + + class Commands: """ Represents the various commands available in the CLI @@ -136,6 +155,18 @@ class Arguments: REGION = 'region' PROFILE = 'profile' PROXY = 'proxy' + CATALOG_CONNECTION_TYPE = 'catalog_connection_type' + CATALOG_AUTHENTICATION_TYPE = 'catalog_authentication_type' + CATALOG_TOKEN_URI = 'catalog_token_uri' + CATALOG_CLIENT_ID = 'catalog_client_id' + CATALOG_CLIENT_SECRET = 'catalog_client_secret' + CATALOG_CLIENT_SCOPE = 'catalog_client_scope' + CATALOG_BEARER_TOKEN = 'catalog_bearer_token' + CATALOG_ROLE_ARN = 'catalog_role_arn' + CATALOG_ROLE_SESSION_NAME = 'catalog_role_session_name' + CATALOG_EXTERNAL_ID = 'catalog_role_external_id' + CATALOG_SIGNING_REGION = 'catalog_signing_region' + CATALOG_SIGNING_NAME = 'catalog_signing_name' class Hints: @@ -180,6 +211,31 @@ class Create: class Update: DEFAULT_BASE_LOCATION = 'A new default base location for the catalog' + class External: + CATALOG_CONNECTION_TYPE = 'The type of external catalog in [ICEBERG, HADOOP].' + CATALOG_AUTHENTICATION_TYPE = 'The type of authentication in [OAUTH, BEARER, SIGV4]' + + CATALOG_TOKEN_URI = '(For authentication type OAUTH) Token server URI' + CATALOG_CLIENT_ID = '(For authentication type OAUTH) oauth client id' + CATALOG_CLIENT_SECRET = '(For authentication type OAUTH) oauth client secret (input-only)' + CATALOG_CLIENT_SCOPE = ('(For authentication type OAUTH) oauth scopes to specify when exchanging ' + 'for a short-lived access token. Multiple can be provided by specifying' + ' this option more than once') + + CATALOG_BEARER_TOKEN = '(For authentication type BEARER) Bearer token (input-only)' + + CATALOG_ROLE_ARN = ('(For authentication type SIGV4) The aws IAM role arn assumed by polaris ' + 'userArn when signing requests') + CATALOG_ROLE_SESSION_NAME = ('(For authentication type SIGV4) The role session name to be used ' + 'by the SigV4 protocol for signing requests') + CATALOG_EXTERNAL_ID = ('(For authentication type SIGV4) An optional external id used to establish ' + 'a trust relationship with AWS in the trust policy') + CATALOG_SIGNING_REGION = ('(For authentication type SIGV4) Region to be used by the SigV4 protocol ' + 'for signing requests') + CATALOG_SIGNING_NAME = ('(For authentication type SIGV4) The service name to be used by the SigV4 ' + 'protocol for signing requests, the default signing name is "execute-api" ' + 'is if not provided') + class Principals: class Create: TYPE = 'The type of principal to create in [SERVICE]' diff --git a/client/python/cli/options/option_tree.py b/client/python/cli/options/option_tree.py index f926230e19..d8c263fd4f 100644 --- a/client/python/cli/options/option_tree.py +++ b/client/python/cli/options/option_tree.py @@ -19,7 +19,7 @@ from dataclasses import dataclass, field from typing import List -from cli.constants import StorageType, CatalogType, PrincipalType, Hints, Commands, Arguments, Subcommands, Actions +from cli.constants import StorageType, CatalogType, PrincipalType, Hints, Commands, Arguments, Subcommands, Actions, ConnectionType, AuthenticationType @dataclass @@ -74,6 +74,26 @@ class OptionTree: Argument(Arguments.CATALOG_ROLE, str, Hints.CatalogRoles.CATALOG_ROLE) ] + _FEDERATION_ARGS = [ + Argument(Arguments.CATALOG_CONNECTION_TYPE, str, + Hints.Catalogs.External.CATALOG_CONNECTION_TYPE, lower=True, + choices=[ct.value for ct in ConnectionType]), + Argument(Arguments.CATALOG_AUTHENTICATION_TYPE, str, + Hints.Catalogs.External.CATALOG_AUTHENTICATION_TYPE, lower=True, + choices=[at.value for at in AuthenticationType]), + Argument(Arguments.CATALOG_TOKEN_URI, str, Hints.Catalogs.External.CATALOG_TOKEN_URI), + Argument(Arguments.CATALOG_CLIENT_ID, str, Hints.Catalogs.External.CATALOG_CLIENT_ID), + Argument(Arguments.CATALOG_CLIENT_SECRET, str, Hints.Catalogs.External.CATALOG_CLIENT_SECRET), + Argument(Arguments.CATALOG_CLIENT_SCOPE, str, + Hints.Catalogs.External.CATALOG_CLIENT_SCOPE, allow_repeats=True), + Argument(Arguments.CATALOG_BEARER_TOKEN, str, Hints.Catalogs.External.CATALOG_BEARER_TOKEN), + Argument(Arguments.CATALOG_ROLE_ARN, str, Hints.Catalogs.External.CATALOG_ROLE_ARN), + Argument(Arguments.CATALOG_ROLE_SESSION_NAME, str, Hints.Catalogs.External.CATALOG_ROLE_SESSION_NAME), + Argument(Arguments.CATALOG_EXTERNAL_ID, str, Hints.Catalogs.External.CATALOG_EXTERNAL_ID), + Argument(Arguments.CATALOG_SIGNING_REGION, str, Hints.Catalogs.External.CATALOG_SIGNING_REGION), + Argument(Arguments.CATALOG_SIGNING_NAME, str, Hints.Catalogs.External.CATALOG_SIGNING_NAME, lower=True) + ] + @staticmethod def get_tree() -> List[Option]: return [ @@ -94,7 +114,7 @@ def get_tree() -> List[Option]: Argument(Arguments.CONSENT_URL, str, Hints.Catalogs.Create.CONSENT_URL), Argument(Arguments.SERVICE_ACCOUNT, str, Hints.Catalogs.Create.SERVICE_ACCOUNT), Argument(Arguments.PROPERTY, str, Hints.PROPERTY, allow_repeats=True), - ], input_name=Arguments.CATALOG), + ] + OptionTree._FEDERATION_ARGS, input_name=Arguments.CATALOG), Option(Subcommands.DELETE, input_name=Arguments.CATALOG), Option(Subcommands.GET, input_name=Arguments.CATALOG), Option(Subcommands.LIST, args=[ @@ -107,7 +127,7 @@ def get_tree() -> List[Option]: Argument(Arguments.REGION, str, Hints.Catalogs.Create.REGION), Argument(Arguments.SET_PROPERTY, str, Hints.SET_PROPERTY, allow_repeats=True), Argument(Arguments.REMOVE_PROPERTY, str, Hints.REMOVE_PROPERTY, allow_repeats=True), - ], input_name=Arguments.CATALOG) + ] + OptionTree._FEDERATION_ARGS, input_name=Arguments.CATALOG) ]), Option(Commands.PRINCIPALS, 'manage principals', children=[ Option(Subcommands.CREATE, args=[ From f721a17297200454109c4b8cc098730cff9db18f Mon Sep 17 00:00:00 2001 From: Eric Maynard Date: Wed, 18 Jun 2025 16:11:06 -0700 Subject: [PATCH 02/11] wire up command --- client/python/cli/command/__init__.py | 15 ++++++++++++++- client/python/cli/command/catalogs.py | 20 ++++++++++++++++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/client/python/cli/command/__init__.py b/client/python/cli/command/__init__.py index 414ce43f50..4dcef3231c 100644 --- a/client/python/cli/command/__init__.py +++ b/client/python/cli/command/__init__.py @@ -40,6 +40,7 @@ def options_get(key, f=lambda x: x): properties = Parser.parse_properties(options_get(Arguments.PROPERTY)) set_properties = Parser.parse_properties(options_get(Arguments.SET_PROPERTY)) remove_properties = options_get(Arguments.REMOVE_PROPERTY) + catalog_client_scopes = options_get(Arguments.CATALOG_CLIENT_SCOPE) command = None if options.command == Commands.CATALOGS: @@ -61,7 +62,19 @@ def options_get(key, f=lambda x: x): catalog_name=options_get(Arguments.CATALOG), properties={} if properties is None else properties, set_properties={} if set_properties is None else set_properties, - remove_properties=[] if remove_properties is None else remove_properties + remove_properties=[] if remove_properties is None else remove_properties, + catalog_connection_type=options_get(Arguments.CATALOG_CONNECTION_TYPE), + catalog_authentication_type=options_get(Arguments.CATALOG_AUTHENTICATION_TYPE), + catalog_token_uri=options_get(Arguments.CATALOG_TOKEN_URI), + catalog_client_id=options_get(Arguments.CATALOG_CLIENT_ID), + catalog_client_secret=options_get(Arguments.CATALOG_CLIENT_SECRET), + catalog_client_scopes=[] if catalog_client_scopes is None else catalog_client_scopes, + catalog_bearer_token=options_get(Arguments.CATALOG_BEARER_TOKEN), + catalog_role_arn=options_get(Arguments.CATALOG_ROLE_ARN), + catalog_role_session_name=options_get(Arguments.CATALOG_ROLE_SESSION_NAME), + catalog_external_id=options_get(Arguments.CATALOG_EXTERNAL_ID), + catalog_signing_region=options_get(Arguments.CATALOG_SIGNING_REGION), + catalog_signing_name=options_get(Arguments.CATALOG_SIGNING_NAME) ) elif options.command == Commands.PRINCIPALS: from cli.command.principals import PrincipalsCommand diff --git a/client/python/cli/command/catalogs.py b/client/python/cli/command/catalogs.py index 9f7cd2015b..ee689bb16c 100644 --- a/client/python/cli/command/catalogs.py +++ b/client/python/cli/command/catalogs.py @@ -59,6 +59,18 @@ class CatalogsCommand(Command): properties: Dict[str, StrictStr] set_properties: Dict[str, StrictStr] remove_properties: List[str] + catalog_connection_type: str + catalog_authentication_type: str + catalog_token_uri: str + catalog_client_id: str + catalog_client_secret: str + catalog_client_scopes: List[str] + catalog_bearer_token: str + catalog_role_arn: str + catalog_role_session_name: str + catalog_external_id: str + catalog_signing_region: str + catalog_signing_name: str def validate(self): if self.catalogs_subcommand == Subcommands.CREATE: @@ -137,15 +149,19 @@ def _build_storage_config_info(self): ) return config + def _build_connection_config_info(self): + pass + def execute(self, api: PolarisDefaultApi) -> None: if self.catalogs_subcommand == Subcommands.CREATE: - config = self._build_storage_config_info() + storage_config = self._build_storage_config_info() + connection_config = self._build_connection_config_info() if self.catalog_type == CatalogType.EXTERNAL.value: request = CreateCatalogRequest( catalog=ExternalCatalog( type=self.catalog_type.upper(), name=self.catalog_name, - storage_config_info=config, + storage_config_info=storage_config, properties=CatalogProperties( default_base_location=self.default_base_location, additional_properties=self.properties From 209b6e231582d0175120b7b3bc55b4acdf43a75e Mon Sep 17 00:00:00 2001 From: Eric Maynard Date: Wed, 18 Jun 2025 16:19:22 -0700 Subject: [PATCH 03/11] start catalogs impl --- client/python/cli/command/catalogs.py | 53 +++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/client/python/cli/command/catalogs.py b/client/python/cli/command/catalogs.py index ee689bb16c..a1598bd712 100644 --- a/client/python/cli/command/catalogs.py +++ b/client/python/cli/command/catalogs.py @@ -22,7 +22,7 @@ from pydantic import StrictStr from cli.command import Command -from cli.constants import StorageType, CatalogType, Subcommands, Arguments +from cli.constants import StorageType, CatalogType, Subcommands, Arguments, AuthenticationType from cli.options.option_tree import Argument from polaris.management import PolarisDefaultApi, Catalog, CreateCatalogRequest, UpdateCatalogRequest, \ StorageConfigInfo, ExternalCatalog, AwsStorageConfigInfo, AzureStorageConfigInfo, GcpStorageConfigInfo, \ @@ -74,12 +74,31 @@ class CatalogsCommand(Command): def validate(self): if self.catalogs_subcommand == Subcommands.CREATE: - if not self.storage_type: - raise Exception(f'Missing required argument:' - f' {Argument.to_flag_name(Arguments.STORAGE_TYPE)}') - if not self.default_base_location: - raise Exception(f'Missing required argument:' - f' {Argument.to_flag_name(Arguments.DEFAULT_BASE_LOCATION)}') + if self.catalog_type != CatalogType.EXTERNAL.value: + if not self.storage_type: + raise Exception(f'Missing required argument:' + f' {Argument.to_flag_name(Arguments.STORAGE_TYPE)}') + if not self.default_base_location: + raise Exception(f'Missing required argument:' + f' {Argument.to_flag_name(Arguments.DEFAULT_BASE_LOCATION)}') + else: + if self.catalog_authentication_type == AuthenticationType.OAUTH.value: + if not self.catalog_token_uri or not self.catalog_client_id \ + or not self.catalog_client_secret or len(self.catalog_client_scopes) == 0: + raise Exception(f"Authentication type 'OAUTH' requires" + f" {Argument.to_flag_name(Arguments.CATALOG_TOKEN_URI)}," + f" {Argument.to_flag_name(Arguments.CATALOG_CLIENT_ID)}," + f" {Argument.to_flag_name(Arguments.CATALOG_CLIENT_SECRET)}," + f" and at least one {Argument.to_flag_name(Arguments.CATALOG_CLIENT_SCOPE)}.") + elif self.catalog_authentication_type == AuthenticationType.BEARER.value: + if not self.catalog_bearer_token: + raise Exception(f"Missing required argument for authentication type 'BEARER':" + f" {Argument.to_flag_name(Arguments.CATALOG_BEARER_TOKEN)}") + elif self.catalog_authentication_type == AuthenticationType.SIGV4.value: + if not self.catalog_role_arn or not self.catalog_role_session_name: + raise Exception(f"Authentication type 'SIGV4 requires" + f" {Argument.to_flag_name(Arguments.CATALOG_ROLE_ARN)}" + f" and {Argument.to_flag_name(Arguments.CATALOG_ROLE_SESSION_NAME)}") if self.storage_type == StorageType.S3.value: if not self.role_arn: @@ -150,7 +169,25 @@ def _build_storage_config_info(self): return config def _build_connection_config_info(self): - pass + auth_params = None + if self.catalog_authentication_type == StorageType.S3.value: + auth_params = AuthenicationParameters( + storage_type=self.storage_type.upper(), + allowed_locations=self.allowed_locations, + role_arn=self.role_arn, + external_id=self.external_id, + user_arn=self.user_arn, + region=self.region + ) + elif self.storage_type == StorageType.AZURE.value: + config = AzureStorageConfigInfo( + storage_type=self.storage_type.upper(), + allowed_locations=self.allowed_locations, + tenant_id=self.tenant_id, + multi_tenant_app_name=self.multi_tenant_app_name, + consent_url=self.consent_url, + ) + return config def execute(self, api: PolarisDefaultApi) -> None: if self.catalogs_subcommand == Subcommands.CREATE: From 5dcabbeec8054086a226442f61a0dea266dbdaff Mon Sep 17 00:00:00 2001 From: Eric Maynard Date: Wed, 18 Jun 2025 16:40:34 -0700 Subject: [PATCH 04/11] wip --- client/python/README.md | 20 +++++++ client/python/cli/command/__init__.py | 2 + client/python/cli/command/catalogs.py | 73 +++++++++++++++++------- client/python/cli/constants.py | 9 ++- client/python/cli/options/option_tree.py | 8 ++- 5 files changed, 87 insertions(+), 25 deletions(-) diff --git a/client/python/README.md b/client/python/README.md index cec9526816..ff94e3e49c 100644 --- a/client/python/README.md +++ b/client/python/README.md @@ -1,3 +1,23 @@ + + -