diff --git a/CHANGELOG.md b/CHANGELOG.md index 453911797f2..5b45a8dfaa3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -136,7 +136,7 @@ request adding CHANGELOG notes for breaking (!) changes and possibly other secti - Added support for `register table` overwrite semantics in the Iceberg REST catalog flow (`overwrite=true`) for internal Polaris catalogs. With overwrite enabled, existing table pointers can be updated to a new metadata location while preserving default behavior for `overwrite=false`. - Added `REGISTER_TABLE_OVERWRITE` authorization operation mapped to `TABLE_FULL_METADATA` for deterministic overwrite authorization. - Added Polaris Spark 4.0 client. - +- Python CLI: added `gcp` as an external catalog authentication type for Iceberg REST federation, enabling CLI creation of GCP-authenticated catalogs such as BigLake without passing Google credential secrets through command-line flags. ### Changes - Added REPL support to Polaris CLI. diff --git a/client/python/apache_polaris/cli/command/catalogs.py b/client/python/apache_polaris/cli/command/catalogs.py index 6d4d261147e..fae73d75261 100644 --- a/client/python/apache_polaris/cli/command/catalogs.py +++ b/client/python/apache_polaris/cli/command/catalogs.py @@ -47,6 +47,7 @@ PolarisCatalog, CatalogProperties, BearerAuthenticationParameters, + GcpAuthenticationParameters, ImplicitAuthenticationParameters, OAuthClientCredentialsParameters, SigV4AuthenticationParameters, @@ -357,6 +358,10 @@ def _build_connection_config_info( authentication_type=self.catalog_authentication_type.upper(), bearer_token=SecretStr(self.catalog_bearer_token), ) + elif self.catalog_authentication_type == AuthenticationType.GCP.value: + auth_params = GcpAuthenticationParameters( + authentication_type=self.catalog_authentication_type.upper() + ) elif self.catalog_authentication_type == AuthenticationType.SIGV4.value: auth_params = SigV4AuthenticationParameters( authentication_type=self.catalog_authentication_type.upper(), diff --git a/client/python/apache_polaris/cli/command/setup.py b/client/python/apache_polaris/cli/command/setup.py index 20d80cc6c48..5b31cf5214f 100644 --- a/client/python/apache_polaris/cli/command/setup.py +++ b/client/python/apache_polaris/cli/command/setup.py @@ -210,6 +210,9 @@ def _serialize_authentication_info(self, auth_params: Any) -> Dict[str, Any]: if auth_params.bearer_token else None ) + elif auth_type == AuthenticationType.GCP.value: + # No extra flags are required for GCP external-catalog authentication. + pass elif auth_type == AuthenticationType.SIGV4.value: auth_data.update( { @@ -811,6 +814,9 @@ def _map_authentication_properties( ) elif auth_type == AuthenticationType.BEARER.value: auth_args["catalog_bearer_token"] = auth_data.get("token") + elif auth_type == AuthenticationType.GCP.value: + # GCP external-catalog auth is server-side/ambient and carries no client secrets. + pass elif auth_type == AuthenticationType.SIGV4.value: auth_args.update( { diff --git a/client/python/apache_polaris/cli/constants.py b/client/python/apache_polaris/cli/constants.py index 933b1e2627c..16fc42b1531 100644 --- a/client/python/apache_polaris/cli/constants.py +++ b/client/python/apache_polaris/cli/constants.py @@ -65,6 +65,7 @@ class AuthenticationType(Enum): OAUTH = "oauth" BEARER = "bearer" + GCP = "gcp" SIGV4 = "sigv4" IMPLICIT = "implicit" diff --git a/client/python/apache_polaris/cli/options/option_tree.py b/client/python/apache_polaris/cli/options/option_tree.py index 83d38c119f0..5ef4d396fb9 100644 --- a/client/python/apache_polaris/cli/options/option_tree.py +++ b/client/python/apache_polaris/cli/options/option_tree.py @@ -121,7 +121,7 @@ class OptionTree: Argument( Arguments.CATALOG_AUTHENTICATION_TYPE, str, - "Authentication type [OAUTH, BEARER, SIGV4, IMPLICIT]", + "Authentication type [OAUTH, BEARER, GCP, SIGV4, IMPLICIT]", lower=True, choices=[at.value for at in AuthenticationType], group="External Catalog Federation: General Options", diff --git a/client/python/tests/test_catalogs_command.py b/client/python/tests/test_catalogs_command.py index 5c040eadcfc..95920e7107a 100644 --- a/client/python/tests/test_catalogs_command.py +++ b/client/python/tests/test_catalogs_command.py @@ -375,6 +375,10 @@ def test_catalog_create_external_options(self) -> None: call_args.catalog.connection_config_info.authentication_parameters.bearer_token.get_secret_value(), "b", ) + self.assertEqual( + call_args.catalog.connection_config_info.authentication_parameters.authentication_type, + "BEARER", + ) def test_catalog_storage_type_exclusivity(self) -> None: mock_client = self.build_mock_client() @@ -658,6 +662,56 @@ def test_external_catalog_oauth(self) -> None: self.assertEqual(call_args.catalog.storage_config_info.storage_type, "GCS") self.assertEqual(call_args.catalog.properties.default_base_location, "dbl") + def test_external_catalog_gcp(self) -> None: + mock_client = self.build_mock_client() + self.mock_execute( + mock_client, + [ + "catalogs", + "create", + "my-catalog", + "--type", + "external", + "--storage-type", + "gcs", + "--default-base-location", + "dbl", + "--catalog-connection-type", + "iceberg-rest", + "--iceberg-remote-catalog-name", + "biglake", + "--catalog-uri", + "https://biglake.googleapis.com/iceberg/v1/restcatalog", + "--catalog-authentication-type", + "gcp", + "--property", + "header.x-goog-user-project=my-billing-project", + ], + ) + call_args = mock_client.create_catalog.call_args[0][0] + self.assertEqual(call_args.catalog.name, "my-catalog") + self.assertEqual(call_args.catalog.type, "EXTERNAL") + self.assertEqual( + call_args.catalog.connection_config_info.connection_type, "ICEBERG_REST" + ) + self.assertEqual( + call_args.catalog.connection_config_info.remote_catalog_name, "biglake" + ) + self.assertEqual( + call_args.catalog.connection_config_info.uri, + "https://biglake.googleapis.com/iceberg/v1/restcatalog", + ) + self.assertEqual( + call_args.catalog.connection_config_info.authentication_parameters.authentication_type, + "GCP", + ) + self.assertEqual(call_args.catalog.storage_config_info.storage_type, "GCS") + self.assertEqual(call_args.catalog.properties.default_base_location, "dbl") + self.assertEqual( + call_args.catalog.properties.additional_properties, + {"header.x-goog-user-project": "my-billing-project"}, + ) + def test_external_catalog_hadoop(self) -> None: mock_client = self.build_mock_client() self.mock_execute( diff --git a/site/content/in-dev/unreleased/command-line-interface.md b/site/content/in-dev/unreleased/command-line-interface.md index d15af03509b..1d1f8829477 100644 --- a/site/content/in-dev/unreleased/command-line-interface.md +++ b/site/content/in-dev/unreleased/command-line-interface.md @@ -199,7 +199,7 @@ External Catalog Federation: General Options: --iceberg-remote-catalog-name ICEBERG_REMOTE_CATALOG_NAME The remote catalog name when federating to an Iceberg REST catalog --hadoop-warehouse HADOOP_WAREHOUSE The warehouse to use when federating to a HADOOP catalog --hive-warehouse HIVE_WAREHOUSE The warehouse to use when federating to a HIVE catalog - --catalog-authentication-type {oauth,bearer,sigv4,implicit} Authentication type [OAUTH, BEARER, SIGV4, IMPLICIT] + --catalog-authentication-type {oauth,bearer,gcp,sigv4,implicit} Authentication type [OAUTH, BEARER, GCP, SIGV4, IMPLICIT] --catalog-service-identity-type {aws_iam} Service identity type [AWS_IAM] --catalog-uri CATALOG_URI The URI of the external catalog diff --git a/site/content/in-dev/unreleased/federation/iceberg-rest-federation.md b/site/content/in-dev/unreleased/federation/iceberg-rest-federation.md index 217df7d24e3..4ae90f592fd 100644 --- a/site/content/in-dev/unreleased/federation/iceberg-rest-federation.md +++ b/site/content/in-dev/unreleased/federation/iceberg-rest-federation.md @@ -30,8 +30,8 @@ REST implementation), enabling a Polaris service to access table and view entiti - **REST endpoint:** The remote service must expose the Iceberg REST specification. Configure firewalls so Polaris can reach the base URI you provide in the connection config. - **Authentication:** Polaris forwards requests using the credentials defined in - `ConnectionConfigInfo.AuthenticationParameters`. OAuth2 client credentials, bearer tokens, and AWS - SigV4 are supported; choose the scheme the remote service expects. + `ConnectionConfigInfo.AuthenticationParameters`. OAuth2 client credentials, bearer tokens, GCP, + and AWS SigV4 are supported; choose the scheme the remote service expects. ## Feature configuration