You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@polaris.apache.org by di...@apache.org on 2026/08/11 21:11:23 UTC

(polaris) branch main updated: Preserve setup entity properties in exports (#5265)

This is an automated email from the ASF dual-hosted git repository.

dimas-b pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/polaris.git


The following commit(s) were added to refs/heads/main by this push:
     new 1301f89938 Preserve setup entity properties in exports (#5265)
1301f89938 is described below

commit 1301f89938b1037e8937831f4579e0ed23949b98
Author: Matt Faltyn <76...@users.noreply.github.com>
AuthorDate: Tue Aug 11 23:11:17 2026 +0200

    Preserve setup entity properties in exports (#5265)
    
    Keep principal and catalog-role properties in setup exports so backup and migration round trips do not drop user-defined metadata.
    
    Fixes #5264
---
 CHANGELOG.md                                       |  2 +
 client/python/apache_polaris/cli/command/setup.py  |  4 ++
 client/python/tests/test_setup_command.py          | 79 +++++++++++++++++++++-
 .../assets/polaris/reference-setup-config.yaml     |  7 +-
 .../unreleased/getting-started/setup-tools.md      |  3 +-
 5 files changed, 91 insertions(+), 4 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index b3e942a7eb..38abee8da9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -46,6 +46,8 @@ request adding CHANGELOG notes for breaking (!) changes and possibly other secti
 
 ### Fixes
 
+- Python CLI `setup export` now preserves user-defined properties on principals and catalog roles,
+  so exported configurations restore that metadata during `setup apply`.
 - Python CLI `setup apply` no longer double-encodes policy content emitted by `setup export`, so
   exported configurations containing policies can be restored.
 - Python CLI `setup export` now includes nested namespaces and policy definitions from those
diff --git a/client/python/apache_polaris/cli/command/setup.py b/client/python/apache_polaris/cli/command/setup.py
index 50522979a0..03a8528eab 100644
--- a/client/python/apache_polaris/cli/command/setup.py
+++ b/client/python/apache_polaris/cli/command/setup.py
@@ -171,6 +171,8 @@ class SetupCommand(Command):
             principals = sorted(api.list_principals().principals, key=lambda p: p.name)
             for p in principals:
                 principal_info: Dict[str, Any] = {"type": PrincipalType.SERVICE.value}
+                if p.properties:
+                    principal_info["properties"] = p.properties
                 try:
                     assigned_roles = api.list_principal_roles_assigned(p.name).roles
                     if assigned_roles:
@@ -396,6 +398,8 @@ class SetupCommand(Command):
             )
             for r in roles:
                 role_info: Dict[str, Any] = {}
+                if r.properties:
+                    role_info["properties"] = r.properties
                 # Assignments
                 assigned_roles_resp = (
                     api.list_assignee_principal_roles_for_catalog_role(
diff --git a/client/python/tests/test_setup_command.py b/client/python/tests/test_setup_command.py
index fe894fb40f..adb75c6623 100644
--- a/client/python/tests/test_setup_command.py
+++ b/client/python/tests/test_setup_command.py
@@ -306,6 +306,84 @@ class TestSetupCommand(CLITestBase):
         mock_client.list_catalog_roles.assert_called_with("my_catalog")
         mock_client.get_catalog.assert_called_with("my_catalog")
 
+    def test_setup_exported_entity_properties_round_trip_through_apply(self) -> None:
+        principal_properties = {"owner": "data-platform"}
+        catalog_role_properties = {"purpose": "migration"}
+        export_client = self.build_mock_client()
+        export_client.list_principals.return_value = SimpleNamespace(
+            principals=[
+                SimpleNamespace(
+                    name="service-user",
+                    properties=principal_properties,
+                )
+            ]
+        )
+        export_client.list_principal_roles_assigned.return_value = SimpleNamespace(
+            roles=[]
+        )
+        export_client.list_catalog_roles.return_value = SimpleNamespace(
+            roles=[
+                SimpleNamespace(
+                    name="catalog-reader",
+                    properties=catalog_role_properties,
+                )
+            ]
+        )
+        export_client.list_assignee_principal_roles_for_catalog_role.return_value = (
+            SimpleNamespace(roles=[])
+        )
+        export_client.list_grants_for_catalog_role.return_value = SimpleNamespace(
+            grants=[]
+        )
+        export_command = SetupCommand(setup_subcommand=Subcommands.EXPORT)
+
+        exported = {
+            "principals": export_command._export_principals(export_client),
+            "catalog_roles": export_command._export_catalog_roles_for_catalog(
+                export_client, "catalog"
+            ),
+        }
+        loaded = yaml.safe_load(yaml.safe_dump(exported))
+
+        self.assertEqual(
+            loaded["principals"]["service-user"]["properties"],
+            principal_properties,
+        )
+        self.assertEqual(
+            loaded["catalog_roles"]["catalog-reader"]["properties"],
+            catalog_role_properties,
+        )
+
+        apply_client = self.build_mock_client()
+        apply_client.list_principals.return_value = SimpleNamespace(principals=[])
+        apply_client.list_catalog_roles.return_value = SimpleNamespace(roles=[])
+        apply_client.create_principal.return_value = SimpleNamespace(
+            credentials=SimpleNamespace(
+                client_id="client-id",
+                client_secret=SimpleNamespace(get_secret_value=lambda: "secret"),
+            )
+        )
+        apply_command = SetupCommand(setup_subcommand=Subcommands.APPLY)
+
+        with patch("sys.stdout", new_callable=io.StringIO):
+            apply_command._create_principals(apply_client, loaded["principals"])
+        apply_command._create_catalog_roles(
+            apply_client,
+            "catalog",
+            loaded["catalog_roles"],
+        )
+
+        principal_request = apply_client.create_principal.call_args.args[0]
+        catalog_role_request = apply_client.create_catalog_role.call_args.args[1]
+        self.assertEqual(
+            principal_request.principal.properties,
+            principal_properties,
+        )
+        self.assertEqual(
+            catalog_role_request.catalog_role.properties,
+            catalog_role_properties,
+        )
+
     @patch("apache_polaris.cli.command.setup.os.path.isfile")
     def test_setup_apply_treats_null_type_as_internal(
         self, mock_isfile: MagicMock
@@ -603,4 +681,3 @@ class TestSetupCommand(CLITestBase):
         self.assertEqual(
             created.storage_config_info.sts_endpoint, "https://sts.amazonaws.com"
         )
-
diff --git a/site/content/guides/assets/polaris/reference-setup-config.yaml b/site/content/guides/assets/polaris/reference-setup-config.yaml
index 8bcf8a76cb..fe5c0c6243 100644
--- a/site/content/guides/assets/polaris/reference-setup-config.yaml
+++ b/site/content/guides/assets/polaris/reference-setup-config.yaml
@@ -33,6 +33,9 @@ principals:
   # A principal for a quickstart user
   quickstart_user:
     type: service
+    # Optional properties persisted with the principal.
+    properties:
+      owner: data-platform
     roles:
       - quickstart_user_role
   # A principal for a readonly user
@@ -75,6 +78,9 @@ catalogs:
     # Catalog roles are scoped to this specific catalog.
     roles:
       quickstart_catalog_role:
+        # Optional properties persisted with the catalog role.
+        properties:
+          purpose: catalog-administration
         # Assigns this catalog role to one or more principal roles.
         assign_to:
           - quickstart_user_role
@@ -227,4 +233,3 @@ catalogs:
   #   service_account: "quickstart-service-account@gcs-project.iam.gserviceaccount.com"
   #   properties:
   #     catalog: quickstart_catalog_gcs
-
diff --git a/site/content/in-dev/unreleased/getting-started/setup-tools.md b/site/content/in-dev/unreleased/getting-started/setup-tools.md
index c96e8eb050..2d5a509a84 100644
--- a/site/content/in-dev/unreleased/getting-started/setup-tools.md
+++ b/site/content/in-dev/unreleased/getting-started/setup-tools.md
@@ -40,7 +40,7 @@ If you already have an Apache Polaris environment, you can export its current st
 polaris setup export --client-id ${CLIENT_ID} --client-secret ${CLIENT_SECRET} > polaris_bootstrap.yaml
 ```
 
-This generates a readable YAML file containing principals, principal roles, catalogs, and their associated namespaces and catalog roles.
+This generates a readable YAML file containing principals, principal roles, catalogs, and their associated namespaces and catalog roles. User-defined properties on principals and catalog roles are preserved in the exported configuration.
 
 ## Applying a Configuration
 
@@ -104,4 +104,3 @@ The current implementation focuses on simplifying initial setup, with a few limi
 - **Policy attachment export**: Policy attachments are not included in `setup export` due to performance considerations. However, they can still be defined in YAML and applied during `setup apply`.
 - **External catalog testing**: Support for external catalogs (e.g., Hive Metastore) exists, but full end-to-end testing has not yet been completed. It is recommended to validate configurations in a non-production environment first.
 
-