From 00478a312cc12c890ba8cbc33ed3d08fdf180944 Mon Sep 17 00:00:00 2001 From: Mathieu Garstecki Date: Fri, 19 Dec 2025 20:05:11 +0100 Subject: [PATCH 1/2] Support the S3NS variant of GCP S3NS is a new deployment of GCP, with operations delegated by Google to Thales: https://www.s3ns.io/en S3NS is officially supported by Google, but is totally separate from the regular GCP, with notably its own API domains. We're porting our platform to S3NS, which includes CNPG configured with the Barman Cloud plugin for backups, which doesn't work there right now because we can't configure the Google *universe*. To connect to S3NS, official clients need to be configured with a universe value, that overrides the base API domain. `GOOGLE_CLOUD_UNIVERSE_DOMAIN` is the official environment variable used to target S3NS: https://documentation.s3ns.fr/docs/overview/tpc-key-differences#key_differences_for_developers Unfortunately Barman uses the v1 client that doesn't read this variable, and only accepts the universe as part of the `client_options` field in the constructor, so we have to pass it in like this. --- .../cloud_providers/google_cloud_storage.py | 7 +++- tests/test_cloud.py | 39 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/barman/cloud_providers/google_cloud_storage.py b/barman/cloud_providers/google_cloud_storage.py index f24c4369b..12a8f5387 100644 --- a/barman/cloud_providers/google_cloud_storage.py +++ b/barman/cloud_providers/google_cloud_storage.py @@ -131,7 +131,12 @@ def _reinit_session(self): Creates a client using "GOOGLE_APPLICATION_CREDENTIALS" env. An error will be raised if the variable is missing. """ - self.client = storage.Client() + client_options = None + universe_domain = os.getenv("GOOGLE_CLOUD_UNIVERSE_DOMAIN") + if universe_domain: + client_options={"universe_domain": universe_domain} + + self.client = storage.Client(client_options=client_options) self.container_client = self.client.bucket(self.bucket_name) def test_connectivity(self): diff --git a/tests/test_cloud.py b/tests/test_cloud.py index 1cb37e717..24b6dd104 100644 --- a/tests/test_cloud.py +++ b/tests/test_cloud.py @@ -2658,6 +2658,45 @@ def test_connectivity_failure(self, gcs_client_mock): container_client_mock.exists.side_effect = GoogleAPIError("error") assert cloud_interface.test_connectivity() is False + @mock.patch.dict(os.environ, {"GOOGLE_CLOUD_UNIVERSE_DOMAIN": "custom.universe.domain"}) + @mock.patch("barman.cloud_providers.google_cloud_storage.storage.Client") + def test_universe_domain_from_environment(self, gcs_client_mock): + """ + Test that the universe domain is properly loaded from GOOGLE_CLOUD_UNIVERSE_DOMAIN + environment variable and passed to the storage client + """ + # GIVEN a GoogleCloudInterface instance is created + cloud_interface = GoogleCloudInterface( + "https://console.cloud.google.com/storage/browser/barman-test/test" + ) + + # THEN the storage.Client should be called with the universe_domain in client_options + gcs_client_mock.assert_called_once_with( + client_options={"universe_domain": "custom.universe.domain"} + ) + + # AND the cloud interface should be properly initialized + assert cloud_interface.client == gcs_client_mock.return_value + assert cloud_interface.container_client == gcs_client_mock.return_value.bucket.return_value + + @mock.patch("barman.cloud_providers.google_cloud_storage.storage.Client") + def test_no_universe_domain_environment(self, gcs_client_mock): + """ + Test that when GOOGLE_CLOUD_UNIVERSE_DOMAIN is not set, the storage client + is created without client_options + """ + # GIVEN a GoogleCloudInterface instance is created without universe domain env var + cloud_interface = GoogleCloudInterface( + "https://console.cloud.google.com/storage/browser/barman-test/test" + ) + + # THEN the storage.Client should be called with client_options=None + gcs_client_mock.assert_called_once_with(client_options=None) + + # AND the cloud interface should be properly initialized + assert cloud_interface.client == gcs_client_mock.return_value + assert cloud_interface.container_client == gcs_client_mock.return_value.bucket.return_value + @mock.patch("barman.cloud_providers.google_cloud_storage.storage.Client") def test_setup_bucket(self, gcs_client_mock): """ From 436a0f823a5c3c0967fc39a92010fd1672e72501 Mon Sep 17 00:00:00 2001 From: Mathieu Garstecki Date: Thu, 8 Jan 2026 15:53:50 +0100 Subject: [PATCH 2/2] fix linter-reported issues --- barman/cloud_providers/google_cloud_storage.py | 2 +- tests/test_cloud.py | 14 +++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/barman/cloud_providers/google_cloud_storage.py b/barman/cloud_providers/google_cloud_storage.py index 12a8f5387..e8da7922e 100644 --- a/barman/cloud_providers/google_cloud_storage.py +++ b/barman/cloud_providers/google_cloud_storage.py @@ -134,7 +134,7 @@ def _reinit_session(self): client_options = None universe_domain = os.getenv("GOOGLE_CLOUD_UNIVERSE_DOMAIN") if universe_domain: - client_options={"universe_domain": universe_domain} + client_options = {"universe_domain": universe_domain} self.client = storage.Client(client_options=client_options) self.container_client = self.client.bucket(self.bucket_name) diff --git a/tests/test_cloud.py b/tests/test_cloud.py index 24b6dd104..eb2775e76 100644 --- a/tests/test_cloud.py +++ b/tests/test_cloud.py @@ -2658,7 +2658,9 @@ def test_connectivity_failure(self, gcs_client_mock): container_client_mock.exists.side_effect = GoogleAPIError("error") assert cloud_interface.test_connectivity() is False - @mock.patch.dict(os.environ, {"GOOGLE_CLOUD_UNIVERSE_DOMAIN": "custom.universe.domain"}) + @mock.patch.dict( + os.environ, {"GOOGLE_CLOUD_UNIVERSE_DOMAIN": "custom.universe.domain"} + ) @mock.patch("barman.cloud_providers.google_cloud_storage.storage.Client") def test_universe_domain_from_environment(self, gcs_client_mock): """ @@ -2677,7 +2679,10 @@ def test_universe_domain_from_environment(self, gcs_client_mock): # AND the cloud interface should be properly initialized assert cloud_interface.client == gcs_client_mock.return_value - assert cloud_interface.container_client == gcs_client_mock.return_value.bucket.return_value + assert ( + cloud_interface.container_client + == gcs_client_mock.return_value.bucket.return_value + ) @mock.patch("barman.cloud_providers.google_cloud_storage.storage.Client") def test_no_universe_domain_environment(self, gcs_client_mock): @@ -2695,7 +2700,10 @@ def test_no_universe_domain_environment(self, gcs_client_mock): # AND the cloud interface should be properly initialized assert cloud_interface.client == gcs_client_mock.return_value - assert cloud_interface.container_client == gcs_client_mock.return_value.bucket.return_value + assert ( + cloud_interface.container_client + == gcs_client_mock.return_value.bucket.return_value + ) @mock.patch("barman.cloud_providers.google_cloud_storage.storage.Client") def test_setup_bucket(self, gcs_client_mock):