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 d4ba44da40b [Java] Honor GOOGLE_CLOUD_UNIVERSE_DOMAIN for the Dataflow 
endpoint (#40672)
d4ba44da40b is described below

commit d4ba44da40b4a638fda3d92ae87caab26b92da31
Author: Shunping Huang <[email protected]>
AuthorDate: Thu Oct 22 18:43:47 2026 -0400

    [Java] Honor GOOGLE_CLOUD_UNIVERSE_DOMAIN for the Dataflow endpoint (#40672)
---
 CHANGES.md                                         |   1 +
 .../options/DataflowPipelineDebugOptions.java      |  17 +++-
 .../dataflow/options/DataflowPipelineOptions.java  |   3 +-
 .../runners/dataflow/util/DataflowTransport.java   |  52 ++++++----
 .../dataflow/util/DataflowTransportTest.java       | 109 +++++++++++++++++++++
 5 files changed, 159 insertions(+), 23 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index 77f03f3da76..39d25b568a0 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -93,6 +93,7 @@
 * (Java) JmsIO migrated to `jakarta.jms` (JMS 3.1) and ActiveMQ 6.2.5. User 
code implementing `JmsIO.MessageMapper`,
   `valueMapper`, `topicNameMapper`, or providing a `ConnectionFactory` must 
update imports from `javax.jms.*` to
   `jakarta.jms.*`. The module now requires Java 17 at runtime 
([#38729](https://github.com/apache/beam/issues/38729)).
+* (Java) Dataflow runner: `--apiRootUrl` no longer defaults to 
`https://dataflow.googleapis.com/`. When neither `--apiRootUrl` nor an absolute 
`--dataflowEndpoint` is set, the Google API 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 
`DataflowPipelineDebugOptions.getApiRootUrl()` should handle a `null` value 
([#40569](https://github.com/apache/beam/issues/40569)).
 * (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)).
diff --git 
a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowPipelineDebugOptions.java
 
b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowPipelineDebugOptions.java
index 7a5284151b9..caa27c546ea 100644
--- 
a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowPipelineDebugOptions.java
+++ 
b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowPipelineDebugOptions.java
@@ -31,6 +31,7 @@ import org.apache.beam.sdk.options.Hidden;
 import org.apache.beam.sdk.options.MemoryMonitorOptions;
 import org.apache.beam.sdk.options.PipelineOptions;
 import org.apache.beam.sdk.util.InstanceBuilder;
+import org.checkerframework.checker.nullness.qual.Nullable;
 
 /**
  * Internal. Options used to control execution of the Dataflow SDK for 
debugging and testing
@@ -47,13 +48,20 @@ public interface DataflowPipelineDebugOptions
    * The root URL for the Dataflow API. {@code dataflowEndpoint} can override 
this value if it
    * contains an absolute URL, otherwise {@code apiRootUrl} will be combined 
with {@code
    * dataflowEndpoint} to generate the full URL to communicate with the 
Dataflow API.
+   *
+   * <p>If not set, the root URL is resolved by the Google API client library, 
which honors {@code
+   * GOOGLE_CLOUD_UNIVERSE_DOMAIN} and otherwise uses the default public URL 
({@value
+   * Dataflow#DEFAULT_ROOT_URL}).
    */
   @Description(
       "The root URL for the Dataflow API. dataflowEndpoint can override this "
           + "value if it contains an absolute URL, otherwise apiRootUrl will 
be combined with "
-          + "dataflowEndpoint to generate the full URL to communicate with the 
Dataflow API.")
-  @Default.String(Dataflow.DEFAULT_ROOT_URL)
-  String getApiRootUrl();
+          + "dataflowEndpoint to generate the full URL to communicate with the 
Dataflow API. "
+          + "If not set, the root URL is resolved by the Google API client 
library, which "
+          + "honors GOOGLE_CLOUD_UNIVERSE_DOMAIN and otherwise uses the 
default public URL ("
+          + Dataflow.DEFAULT_ROOT_URL
+          + ").")
+  @Nullable String getApiRootUrl();
 
   void setApiRootUrl(String value);
 
@@ -64,7 +72,8 @@ public interface DataflowPipelineDebugOptions
    * SDK version was released.
    *
    * <p>If the string contains "://", then this is treated as a URL, otherwise 
{@link
-   * #getApiRootUrl()} is used as the root URL.
+   * #getApiRootUrl()} is used as the root URL (or, if that is not set, the 
root URL resolved by the
+   * Google API client library).
    */
   @Description(
       "The URL for the Dataflow API. If the string contains \"://\", this"
diff --git 
a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowPipelineOptions.java
 
b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowPipelineOptions.java
index 57f927d7307..02ae3bc762b 100644
--- 
a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowPipelineOptions.java
+++ 
b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowPipelineOptions.java
@@ -161,7 +161,8 @@ public interface DataflowPipelineOptions
    * SDK version was released.
    *
    * <p>If the string contains "://", then this is treated as a URL, otherwise 
{@link
-   * #getApiRootUrl()} is used as the root URL.
+   * #getApiRootUrl()} is used as the root URL (or, if that is not set, the 
root URL resolved by the
+   * Google API client library).
    */
   @Description(
       "The URL for the Dataflow API. If the string contains \"://\", this"
diff --git 
a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/util/DataflowTransport.java
 
b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/util/DataflowTransport.java
index ad579333ebf..a93c3dec25d 100644
--- 
a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/util/DataflowTransport.java
+++ 
b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/util/DataflowTransport.java
@@ -30,7 +30,9 @@ import java.net.URL;
 import org.apache.beam.runners.dataflow.options.DataflowPipelineOptions;
 import org.apache.beam.sdk.extensions.gcp.auth.NullCredentialInitializer;
 import org.apache.beam.sdk.extensions.gcp.util.RetryHttpRequestInitializer;
+import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Strings;
 import 
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList;
+import org.checkerframework.checker.nullness.qual.Nullable;
 
 /** Helpers for cloud communication. */
 public class DataflowTransport {
@@ -59,28 +61,42 @@ public class DataflowTransport {
     }
   }
 
-  /** Returns a Google Cloud Dataflow client builder. */
+  /**
+   * Returns a Google Cloud Dataflow client builder.
+   *
+   * <p>The root URL and service path are only set on the builder when the 
user configured them via
+   * {@code dataflowEndpoint} and/or {@code apiRootUrl}. Otherwise, the 
endpoint is resolved by the
+   * Google API client library, which honors {@code 
GOOGLE_CLOUD_UNIVERSE_DOMAIN} and otherwise uses
+   * the default public URL. Note that calling {@link 
Dataflow.Builder#setRootUrl} (even with the
+   * default URL) disables this resolution.
+   */
   public static Dataflow.Builder newDataflowClient(DataflowPipelineOptions 
options) {
+    Dataflow.Builder builder =
+        new Dataflow.Builder(
+                getTransport(),
+                getJsonFactory(),
+                chainHttpRequestInitializer(
+                    options.getGcpCredential(),
+                    // Do not log 404. It clutters the output and is possibly 
even required by the
+                    // caller.
+                    new RetryHttpRequestInitializer(ImmutableList.of(404))))
+            .setApplicationName(options.getAppName())
+            .setGoogleClientRequestInitializer(options.getGoogleApiTrace());
+
     String servicePath = options.getDataflowEndpoint();
-    ApiComponents components;
-    if (servicePath.contains("://")) {
-      components = apiComponentsFromUrl(servicePath);
+    @Nullable String apiRootUrl = options.getApiRootUrl();
+    if (!Strings.isNullOrEmpty(servicePath) && servicePath.contains("://")) {
+      ApiComponents components = apiComponentsFromUrl(servicePath);
+      
builder.setRootUrl(components.rootUrl).setServicePath(components.servicePath);
     } else {
-      components = new ApiComponents(options.getApiRootUrl(), servicePath);
+      if (apiRootUrl != null && !apiRootUrl.isEmpty()) {
+        builder.setRootUrl(apiRootUrl);
+      }
+      if (!Strings.isNullOrEmpty(servicePath)) {
+        builder.setServicePath(servicePath);
+      }
     }
-
-    return new Dataflow.Builder(
-            getTransport(),
-            getJsonFactory(),
-            chainHttpRequestInitializer(
-                options.getGcpCredential(),
-                // Do not log 404. It clutters the output and is possibly even 
required by the
-                // caller.
-                new RetryHttpRequestInitializer(ImmutableList.of(404))))
-        .setApplicationName(options.getAppName())
-        .setRootUrl(components.rootUrl)
-        .setServicePath(components.servicePath)
-        .setGoogleClientRequestInitializer(options.getGoogleApiTrace());
+    return builder;
   }
 
   private static HttpRequestInitializer chainHttpRequestInitializer(
diff --git 
a/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/util/DataflowTransportTest.java
 
b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/util/DataflowTransportTest.java
new file mode 100644
index 00000000000..1cb34430a21
--- /dev/null
+++ 
b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/util/DataflowTransportTest.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.beam.runners.dataflow.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assume.assumeTrue;
+
+import com.google.api.services.dataflow.Dataflow;
+import org.apache.beam.runners.dataflow.options.DataflowPipelineOptions;
+import org.apache.beam.sdk.extensions.gcp.auth.TestCredential;
+import org.apache.beam.sdk.options.PipelineOptionsFactory;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link DataflowTransport}. */
+@RunWith(JUnit4.class)
+public class DataflowTransportTest {
+
+  private static final String UNIVERSE_DOMAIN = "example-universe.goog";
+
+  private DataflowPipelineOptions options;
+
+  @Before
+  public void setUp() {
+    options = 
PipelineOptionsFactory.create().as(DataflowPipelineOptions.class);
+    options.setGcpCredential(new TestCredential());
+  }
+
+  @Test
+  public void testApiRootUrlIsUnsetByDefault() {
+    assertNull(options.getApiRootUrl());
+    assertEquals("", options.getDataflowEndpoint());
+  }
+
+  @Test
+  public void testDefaultEndpoint() {
+    // The default endpoint is resolved by the client library, which honors
+    // GOOGLE_CLOUD_UNIVERSE_DOMAIN. Only assert the public default if it is 
not set.
+    assumeTrue(System.getenv("GOOGLE_CLOUD_UNIVERSE_DOMAIN") == null);
+    Dataflow client = DataflowTransport.newDataflowClient(options).build();
+    assertEquals(Dataflow.DEFAULT_BASE_URL, client.getBaseUrl());
+  }
+
+  @Test
+  public void testDefaultEndpointHonorsUniverseDomain() {
+    Dataflow client =
+        
DataflowTransport.newDataflowClient(options).setUniverseDomain(UNIVERSE_DOMAIN).build();
+    assertEquals("https://dataflow."; + UNIVERSE_DOMAIN + "/", 
client.getBaseUrl());
+  }
+
+  @Test
+  public void testRelativeDataflowEndpointHonorsUniverseDomain() {
+    options.setDataflowEndpoint("v0neverExisted");
+    Dataflow client =
+        
DataflowTransport.newDataflowClient(options).setUniverseDomain(UNIVERSE_DOMAIN).build();
+    assertEquals("https://dataflow."; + UNIVERSE_DOMAIN + "/v0neverExisted/", 
client.getBaseUrl());
+  }
+
+  @Test
+  public void testAbsoluteDataflowEndpointOverridesUniverseDomain() {
+    
options.setDataflowEndpoint("https://custom-dataflow.example.com:8443/some/path/";);
+    Dataflow client =
+        
DataflowTransport.newDataflowClient(options).setUniverseDomain(UNIVERSE_DOMAIN).build();
+    assertEquals("https://custom-dataflow.example.com:8443/";, 
client.getRootUrl());
+    assertEquals("some/path/", client.getServicePath());
+  }
+
+  @Test
+  public void testApiRootUrlOverridesUniverseDomain() {
+    options.setApiRootUrl("https://custom-dataflow.example.com/";);
+    Dataflow client =
+        
DataflowTransport.newDataflowClient(options).setUniverseDomain(UNIVERSE_DOMAIN).build();
+    assertEquals("https://custom-dataflow.example.com/";, client.getBaseUrl());
+  }
+
+  @Test
+  public void testApiRootUrlWithRelativeDataflowEndpoint() {
+    options.setApiRootUrl("https://custom-dataflow.example.com/";);
+    options.setDataflowEndpoint("v0neverExisted");
+    Dataflow client = DataflowTransport.newDataflowClient(options).build();
+    assertEquals("https://custom-dataflow.example.com/v0neverExisted/";, 
client.getBaseUrl());
+  }
+
+  @Test
+  public void testExplicitDefaultApiRootUrlIsNotOverridden() {
+    options.setApiRootUrl(Dataflow.DEFAULT_ROOT_URL);
+    Dataflow client =
+        
DataflowTransport.newDataflowClient(options).setUniverseDomain(UNIVERSE_DOMAIN).build();
+    assertEquals(Dataflow.DEFAULT_ROOT_URL, client.getBaseUrl());
+  }
+}

Reply via email to