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

shunping pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git


The following commit(s) were added to refs/heads/master by this push:
     new 1a017df4529 [Python] Don't override the Dataflow endpoint by default 
(#40568)
1a017df4529 is described below

commit 1a017df4529bc6b005eaa2a4d12ddb63fa2569f5
Author: Shunping Huang <[email protected]>
AuthorDate: Thu Oct 15 09:05:15 2026 -0400

    [Python] Don't override the Dataflow endpoint by default (#40568)
    
    * [Python] Don't override the Dataflow endpoint by default
    
    The default --dataflow_endpoint (GDU URL) was always passed as an
    api_endpoint override, ignoring GOOGLE_CLOUD_UNIVERSE_DOMAIN (e.g. TPC).
    
    Default it to '' so the client library resolves the endpoint, and drop
    the legacy TaskRunner worker settings that hard-coded the GDU URL.
    
    * Reformat
    
    * Edited CHANGES.md
    
    * Print out the actual endpoint where the job request is sent to.
    
    * Reword comments
---
 CHANGES.md                                         |  1 +
 .../python/apache_beam/options/pipeline_options.py |  8 ++-
 .../options/pipeline_options_validator.py          | 13 ++--
 .../runners/dataflow/internal/apiclient.py         |  9 +--
 .../runners/dataflow/internal/apiclient_test.py    | 70 +++++++++++++++++-----
 5 files changed, 70 insertions(+), 31 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index f98790bc2e1..14c7e676f18 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -84,6 +84,7 @@
   `jakarta.jms.*`. The module now requires Java 17 at runtime 
([#38729](https://github.com/apache/beam/issues/38729)).
 * (Python) `Regex.find_all` with `group=Regex.ALL` now returns every group, as 
`(match, group1, group2, ...)`, instead of only group 1, and with 
`outputEmpty=False` it drops a match only when the whole match is empty, 
matching the `group=0` behavior 
([#40399](https://github.com/apache/beam/pull/40399)).
 * (Python) `Regex.all_matches` now returns every group of the regex, instead 
of failing with a `TypeError` when the regex has no groups and dropping nested 
or unmatched trailing groups 
([#40562](https://github.com/apache/beam/issues/40562)).
+* (Python) `--dataflow_endpoint` now defaults to `''` instead of 
`https://dataflow.googleapis.com`. When unset, the Google Cloud client library 
resolves the Dataflow endpoint and honors `GOOGLE_CLOUD_UNIVERSE_DOMAIN`, so 
Dataflow jobs can be submitted in non-default universes. Code that reads 
`GoogleCloudOptions.dataflow_endpoint` should handle an empty value 
([#40569](https://github.com/apache/beam/issues/40569)).
 
 ## Deprecations
 
diff --git a/sdks/python/apache_beam/options/pipeline_options.py 
b/sdks/python/apache_beam/options/pipeline_options.py
index 84c0743ceaa..08ba34dabee 100644
--- a/sdks/python/apache_beam/options/pipeline_options.py
+++ b/sdks/python/apache_beam/options/pipeline_options.py
@@ -1038,10 +1038,12 @@ class GoogleCloudOptions(PipelineOptions):
   def _add_argparse_args(cls, parser):
     parser.add_argument(
         '--dataflow_endpoint',
-        default=cls.DATAFLOW_ENDPOINT,
+        default='',
         help=(
-            'The URL for the Dataflow API. If not set, the default public URL '
-            'will be used.'))
+            'The URL for the Dataflow API. If not set, the endpoint is '
+            'resolved by the Google Cloud client library, which honors '
+            'GOOGLE_CLOUD_UNIVERSE_DOMAIN and otherwise uses the default '
+            'public URL (%s).' % cls.DATAFLOW_ENDPOINT))
     # Remote execution must check that this option is not None.
     parser.add_argument(
         '--project',
diff --git a/sdks/python/apache_beam/options/pipeline_options_validator.py 
b/sdks/python/apache_beam/options/pipeline_options_validator.py
index 9b0d2e6109d..54505154641 100644
--- a/sdks/python/apache_beam/options/pipeline_options_validator.py
+++ b/sdks/python/apache_beam/options/pipeline_options_validator.py
@@ -168,9 +168,9 @@ class PipelineOptionsValidator(object):
 
     dataflow_endpoint = (
         self.options.view_as(GoogleCloudOptions).dataflow_endpoint)
-    if dataflow_endpoint is None:
-      return False
-    else:
+    # An unset endpoint means the default Dataflow service endpoint,
+    # so only an explicit localhost endpoint opts out.
+    if dataflow_endpoint:
       endpoint_parts = urlparse(dataflow_endpoint, allow_fragments=False)
       if endpoint_parts.netloc.startswith("localhost"):
         return False
@@ -274,10 +274,9 @@ class PipelineOptionsValidator(object):
       else:
         view.region = default_region
     dataflow_endpoint = view.dataflow_endpoint
-    if dataflow_endpoint is None:
-      errors.extend(
-          self._validate_error(self.ERR_MISSING_OPTION, dataflow_endpoint))
-    else:
+    # When dataflow_endpoint is unset, the client library resolves
+    # the endpoint (honoring GOOGLE_CLOUD_UNIVERSE_DOMAIN).
+    if dataflow_endpoint:
       valid_endpoint = self.validate_endpoint_url(dataflow_endpoint)
       if valid_endpoint is False:
         errors.extend(
diff --git a/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py 
b/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py
index 654808373f2..8a478f3fb63 100644
--- a/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py
+++ b/sdks/python/apache_beam/runners/dataflow/internal/apiclient.py
@@ -180,12 +180,7 @@ class Environment(object):
               name=package))
 
     pool = dataflow.WorkerPool(
-        kind='local' if self.local else 'harness',
-        packages=package_descriptors,
-        taskrunner_settings=dataflow.TaskRunnerSettings(
-            parallel_worker_settings=dataflow.WorkerSettings(
-                base_url=GoogleCloudOptions.DATAFLOW_ENDPOINT,
-                service_path=self.google_cloud_options.dataflow_endpoint)))
+        kind='local' if self.local else 'harness', 
packages=package_descriptors)
 
     pool.autoscaling_settings = dataflow.AutoscalingSettings()
     # Set worker pool options received through command line.
@@ -902,7 +897,7 @@ class DataflowApplicationClient(object):
           'HTTP status %d trying to create job'
           ' at dataflow service endpoint %s',
           e.code,
-          self.google_cloud_options.dataflow_endpoint)
+          self._jobs_client.api_endpoint)
       _LOGGER.fatal('details of server error: %s', e)
       raise
 
diff --git 
a/sdks/python/apache_beam/runners/dataflow/internal/apiclient_test.py 
b/sdks/python/apache_beam/runners/dataflow/internal/apiclient_test.py
index fdce35b4b5e..b82c7f55067 100644
--- a/sdks/python/apache_beam/runners/dataflow/internal/apiclient_test.py
+++ b/sdks/python/apache_beam/runners/dataflow/internal/apiclient_test.py
@@ -1320,12 +1320,19 @@ class UtilTest(unittest.TestCase):
           self.assertFalse(template_obj.get('steps'))
           self.assertTrue(template_obj['stepsLocation'])
 
+  @mock.patch.dict(
+      os.environ, {'GOOGLE_CLOUD_UNIVERSE_DOMAIN': 'custom-universe.com'})
   def test_dataflow_endpoint_clean(self):
+    # An explicit endpoint is cleaned and passed as an api_endpoint override to
+    # every Dataflow client, regardless of GOOGLE_CLOUD_UNIVERSE_DOMAIN.
     endpoints_and_expectations = [
         # (input_endpoint, expected_endpoint, expected_transport)
         ('https://dataflow.googleapis.com/', 'dataflow.googleapis.com', None),
         ('https://dataflow.googleapis.com   ', 'dataflow.googleapis.com', 
None),
         ('dataflow.googleapis.com/', 'dataflow.googleapis.com', None),
+        ('https://dataflow.example.com/', 'dataflow.example.com', None),
+        ('https://dataflow.example.com   ', 'dataflow.example.com', None),
+        ('dataflow.example.com/', 'dataflow.example.com', None),
         ('http://localhost:8080/', 'http://localhost:8080', 'rest'),
         ('localhost:8080/', 'localhost:8080', 'rest'),
     ]
@@ -1341,20 +1348,55 @@ class UtilTest(unittest.TestCase):
           '--no_auth',
       ])
       with 
mock.patch('apache_beam.runners.dataflow.internal.apiclient.dataflow'
-                      '.JobsV1Beta3Client') as mock_jobs:
-        with mock.patch(
-            'apache_beam.runners.dataflow.internal.apiclient.dataflow'
-            '.MessagesV1Beta3Client'):
-          with mock.patch(
-              'apache_beam.runners.dataflow.internal.apiclient.dataflow'
-              '.MetricsV1Beta3Client'):
-            apiclient.DataflowApplicationClient(pipeline_options)
-            mock_jobs.assert_called_once()
-            called_kwargs = mock_jobs.call_args.kwargs
-            client_opts = called_kwargs.get('client_options')
-            self.assertIsNotNone(client_opts)
-            self.assertEqual(client_opts.api_endpoint, expected_ep)
-            self.assertEqual(called_kwargs.get('transport'), 
expected_transport)
+                      '.JobsV1Beta3Client') as mock_jobs, \
+          mock.patch('apache_beam.runners.dataflow.internal.apiclient.dataflow'
+                     '.MessagesV1Beta3Client') as mock_messages, \
+          mock.patch('apache_beam.runners.dataflow.internal.apiclient.dataflow'
+                     '.MetricsV1Beta3Client') as mock_metrics:
+        apiclient.DataflowApplicationClient(pipeline_options)
+        for mock_client in (mock_jobs, mock_messages, mock_metrics):
+          mock_client.assert_called_once()
+          called_kwargs = mock_client.call_args.kwargs
+          client_opts = called_kwargs.get('client_options')
+          self.assertIsNotNone(client_opts)
+          self.assertEqual(client_opts.api_endpoint, expected_ep)
+          self.assertEqual(called_kwargs.get('transport'), expected_transport)
+
+  @mock.patch.dict(
+      os.environ,
+      {
+          # Pin mTLS off so endpoint resolution doesn't depend on the host.
+          'GOOGLE_API_USE_CLIENT_CERTIFICATE': 'false',
+          'GOOGLE_API_USE_MTLS_ENDPOINT': 'never',
+      })
+  def test_dataflow_endpoint_unset_uses_client_library_default(self):
+    # Without an explicit endpoint, no api_endpoint override is passed, so the
+    # client library resolves the endpoint and honors
+    # GOOGLE_CLOUD_UNIVERSE_DOMAIN.
+    pipeline_options = PipelineOptions([
+        '--project',
+        'test-project',
+        '--temp_location',
+        'gs://test-location/temp',
+        '--no_auth',
+    ])
+    self.assertEqual(
+        pipeline_options.view_as(GoogleCloudOptions).dataflow_endpoint, '')
+
+    def resolved_endpoints():
+      client = apiclient.DataflowApplicationClient(pipeline_options)
+      return {
+          c.api_endpoint
+          for c in (
+              client._jobs_client, client._messages_client,
+              client._metrics_client)
+      }
+
+    os.environ.pop('GOOGLE_CLOUD_UNIVERSE_DOMAIN', None)
+    self.assertEqual(resolved_endpoints(), {'dataflow.googleapis.com'})
+
+    os.environ['GOOGLE_CLOUD_UNIVERSE_DOMAIN'] = 'custom-universe.com'
+    self.assertEqual(resolved_endpoints(), {'dataflow.custom-universe.com'})
 
   def test_stage_resources(self):
     pipeline_options = PipelineOptions([

Reply via email to