Skip to content

Commit 586ac9f

Browse files
authored
feat: Add Universe Domain to Java-Core (#2329)
* feat: Add Java-Core Universe Domain changes * chore: Move validate universe domain logic to ServiceOptions * chore: Add javadocs * chore: Add tests * chore: Fix lint issues * chore: Add project id to tests * chore: Fix format issues * chore: Address PR comments * chore: Update Apiary to return rootHostUrl * chore: Use Google Auth Library v1.21.0 * chore: Add tests for normalizeEndpoint() * chore: Address PR comments * chore: Address PR comments * chore: Fix comments * chore: Address PR comments * chore: Address PR comments * chore: Add links * chore: Add format to match DEFAULT_HOST * chore: Fix failing tests * chore: Update javadocs * chore: Remove www. prefix
1 parent 4175adb commit 586ac9f

File tree

2 files changed

+253
-4
lines changed

2 files changed

+253
-4
lines changed

java-core/google-cloud-core/src/main/java/com/google/cloud/ServiceOptions.java

+91-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,6 @@ public abstract class ServiceOptions<
8181
implements Serializable {
8282

8383
public static final String CREDENTIAL_ENV_NAME = "GOOGLE_APPLICATION_CREDENTIALS";
84-
8584
private static final String DEFAULT_HOST = "https://www.googleapis.com";
8685
private static final String LEGACY_PROJECT_ENV_NAME = "GCLOUD_PROJECT";
8786
private static final String PROJECT_ENV_NAME = "GOOGLE_CLOUD_PROJECT";
@@ -95,6 +94,7 @@ public abstract class ServiceOptions<
9594
protected final String clientLibToken;
9695

9796
private final String projectId;
97+
private final String universeDomain;
9898
private final String host;
9999
private final RetrySettings retrySettings;
100100
private final String serviceRpcFactoryClassName;
@@ -125,6 +125,7 @@ public abstract static class Builder<
125125
private final ImmutableSet<String> allowedClientLibTokens =
126126
ImmutableSet.of(ServiceOptions.getGoogApiClientLibName());
127127
private String projectId;
128+
private String universeDomain;
128129
private String host;
129130
protected Credentials credentials;
130131
private RetrySettings retrySettings;
@@ -142,6 +143,7 @@ protected Builder() {}
142143
@InternalApi("This class should only be extended within google-cloud-java")
143144
protected Builder(ServiceOptions<ServiceT, OptionsT> options) {
144145
projectId = options.projectId;
146+
universeDomain = options.universeDomain;
145147
host = options.host;
146148
credentials = options.credentials;
147149
retrySettings = options.retrySettings;
@@ -199,6 +201,22 @@ public B setHost(String host) {
199201
return self();
200202
}
201203

204+
/**
205+
* Universe Domain is the domain for Google Cloud Services. A Google Cloud endpoint follows the
206+
* format of `{ServiceName}.{UniverseDomain}`. For example, speech.googleapis.com would have a
207+
* Universe Domain value of `googleapis.com` and cloudasset.test.com would have a Universe
208+
* Domain of `test.com`.
209+
*
210+
* <p>If this value is not set, the resolved UniverseDomain will default to `googleapis.com`.
211+
*
212+
* @throws NullPointerException if {@code universeDomain} is {@code null}. The resolved
213+
* universeDomain will be `googleapis.com` if this value is not set.
214+
*/
215+
public B setUniverseDomain(String universeDomain) {
216+
this.universeDomain = checkNotNull(universeDomain);
217+
return self();
218+
}
219+
202220
/**
203221
* Sets the service authentication credentials. If no credentials are set, {@link
204222
* GoogleCredentials#getApplicationDefault()} will be used to attempt getting credentials from
@@ -306,6 +324,7 @@ protected ServiceOptions(
306324
"A project ID is required for this service but could not be determined from the builder "
307325
+ "or the environment. Please set a project ID using the builder.");
308326
}
327+
universeDomain = builder.universeDomain;
309328
host = firstNonNull(builder.host, getDefaultHost());
310329
credentials = builder.credentials != null ? builder.credentials : defaultCredentials();
311330
retrySettings = firstNonNull(builder.retrySettings, getDefaultRetrySettings());
@@ -582,6 +601,19 @@ public String getProjectId() {
582601
return projectId;
583602
}
584603

604+
/**
605+
* Universe Domain is the domain for Google Cloud Services. A Google Cloud endpoint follows the
606+
* format of `{ServiceName}.{UniverseDomain}`. For example, speech.googleapis.com would have a
607+
* Universe Domain value of `googleapis.com` and cloudasset.test.com would have a Universe Domain
608+
* of `test.com`.
609+
*
610+
* @return The universe domain value set in the Builder's setter. This is not the resolved
611+
* Universe Domain
612+
*/
613+
public String getUniverseDomain() {
614+
return universeDomain;
615+
}
616+
585617
/** Returns the service host. */
586618
public String getHost() {
587619
return host;
@@ -767,4 +799,62 @@ public String getClientLibToken() {
767799
public String getQuotaProjectId() {
768800
return quotaProjectId;
769801
}
802+
803+
/**
804+
* Returns the resolved host for the Service to connect to Google Cloud
805+
*
806+
* <p>The resolved host will be in `https://{serviceName}.{resolvedUniverseDomain}` format. The
807+
* resolvedUniverseDomain will be set to `googleapis.com` if universeDomain is null. The format is
808+
* similar to the DEFAULT_HOST value in java-core.
809+
*
810+
* @see <a
811+
* href="https://github.com/googleapis/sdk-platform-java/blob/097964f24fa1989bc74b4807a253f0be4e9dd1ea/java-core/google-cloud-core/src/main/java/com/google/cloud/ServiceOptions.java#L85">DEFAULT_HOST</a>
812+
*/
813+
@InternalApi
814+
public String getResolvedHost(String serviceName) {
815+
if (universeDomain != null && universeDomain.isEmpty()) {
816+
throw new IllegalArgumentException("The universe domain cannot be empty");
817+
}
818+
String resolvedUniverseDomain =
819+
universeDomain != null ? universeDomain : Credentials.GOOGLE_DEFAULT_UNIVERSE;
820+
// The host value set to DEFAULT_HOST if the user didn't configure a host. If the
821+
// user set a host the library uses that value, otherwise, construct the host for the user.
822+
// The DEFAULT_HOST value is not a valid host for handwritten libraries and should be
823+
// overriden to include the serviceName.
824+
if (!DEFAULT_HOST.equals(host)) {
825+
return host;
826+
}
827+
return "https://" + serviceName + "." + resolvedUniverseDomain;
828+
}
829+
830+
/**
831+
* Temporarily used for BigQuery and Storage Apiary Wrapped Libraries. To be removed in the future
832+
* when Apiary clients can resolve their endpoints. Returns the host to be used as the rootUrl.
833+
*
834+
* <p>The resolved host will be in `https://{serviceName}.{resolvedUniverseDomain}/` format. The
835+
* resolvedUniverseDomain will be set to `googleapis.com` if universeDomain is null.
836+
*
837+
* @see <a
838+
* href="https://github.com/googleapis/google-api-java-client/blob/76765d5f9689be9d266a7d62fa6ffb4cabf701f5/google-api-client/src/main/java/com/google/api/client/googleapis/services/AbstractGoogleClient.java#L49">rootUrl</a>
839+
*/
840+
@InternalApi
841+
public String getResolvedApiaryHost(String serviceName) {
842+
String resolvedUniverseDomain =
843+
universeDomain != null ? universeDomain : Credentials.GOOGLE_DEFAULT_UNIVERSE;
844+
return "https://" + serviceName + "." + resolvedUniverseDomain + "/";
845+
}
846+
847+
/**
848+
* Validates that Credentials' Universe Domain matches the resolved Universe Domain. Currently,
849+
* this is only intended for BigQuery and Storage Apiary Wrapped Libraries.
850+
*
851+
* <p>This validation call should be made prior to any RPC invocation. This call is used to gate
852+
* the RPC invocation if there is no valid universe domain.
853+
*/
854+
@InternalApi
855+
public boolean hasValidUniverseDomain() throws IOException {
856+
String resolvedUniverseDomain =
857+
universeDomain != null ? universeDomain : Credentials.GOOGLE_DEFAULT_UNIVERSE;
858+
return resolvedUniverseDomain.equals(getCredentials().getUniverseDomain());
859+
}
770860
}

java-core/google-cloud-core/src/test/java/com/google/cloud/ServiceOptionsTest.java

+162-3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import static org.junit.Assert.assertNotEquals;
2323
import static org.junit.Assert.assertNull;
2424
import static org.junit.Assert.assertSame;
25+
import static org.junit.Assert.assertThrows;
2526
import static org.junit.Assert.assertTrue;
2627
import static org.junit.Assert.fail;
2728

@@ -55,6 +56,7 @@ public class ServiceOptionsTest {
5556
private static GoogleCredentials credentials;
5657
private static GoogleCredentials credentialsWithProjectId;
5758
private static GoogleCredentials credentialsWithQuotaProject;
59+
private static GoogleCredentials credentialsNotInGDU;
5860

5961
private static final String JSON_KEY =
6062
"{\n"
@@ -81,7 +83,8 @@ public class ServiceOptionsTest {
8183
+ "XyRDW4IG1Oa2p\\nrALStNBx5Y9t0/LQnFI4w3aG\\n-----END PRIVATE KEY-----\\n\",\n"
8284
+ " \"client_email\": \"[email protected]\",\n"
8385
+ " \"client_id\": \"someclientid.apps.googleusercontent.com\",\n"
84-
+ " \"type\": \"service_account\"\n"
86+
+ " \"type\": \"service_account\",\n"
87+
+ " \"universe_domain\": \"googleapis.com\"\n"
8588
+ "}";
8689

8790
private static final String JSON_KEY_PROJECT_ID =
@@ -110,7 +113,8 @@ public class ServiceOptionsTest {
110113
+ " \"project_id\": \"someprojectid\",\n"
111114
+ " \"client_email\": \"[email protected]\",\n"
112115
+ " \"client_id\": \"someclientid.apps.googleusercontent.com\",\n"
113-
+ " \"type\": \"service_account\"\n"
116+
+ " \"type\": \"service_account\",\n"
117+
+ " \"universe_domain\": \"googleapis.com\"\n"
114118
+ "}";
115119

116120
private static final String JSON_KEY_QUOTA_PROJECT_ID =
@@ -140,13 +144,45 @@ public class ServiceOptionsTest {
140144
+ " \"client_email\": \"[email protected]\",\n"
141145
+ " \"client_id\": \"someclientid.apps.googleusercontent.com\",\n"
142146
+ " \"type\": \"service_account\",\n"
143-
+ " \"quota_project_id\": \"some-quota-project-id\"\n"
147+
+ " \"quota_project_id\": \"some-quota-project-id\",\n"
148+
+ " \"universe_domain\": \"googleapis.com\"\n"
149+
+ "}";
150+
151+
// Key added by copying the keys above and adding in the universe domain field
152+
private static final String JSON_KEY_NON_GDU =
153+
"{\n"
154+
+ " \"private_key_id\": \"somekeyid\",\n"
155+
+ " \"private_key\": \"-----BEGIN PRIVATE KEY-----\\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggS"
156+
+ "kAgEAAoIBAQC+K2hSuFpAdrJI\\nnCgcDz2M7t7bjdlsadsasad+fvRSW6TjNQZ3p5LLQY1kSZRqBqylRkzteMOyHg"
157+
+ "aR\\n0Pmxh3ILCND5men43j3h4eDbrhQBuxfEMalkG92sL+PNQSETY2tnvXryOvmBRwa/\\nQP/9dJfIkIDJ9Fw9N4"
158+
+ "Bhhhp6mCcRpdQjV38H7JsyJ7lih/oNjECgYAt\\nknddadwkwewcVxHFhcZJO+XWf6ofLUXpRwiTZakGMn8EE1uVa2"
159+
+ "LgczOjwWHGi99MFjxSer5m9\\n1tCa3/KEGKiS/YL71JvjwX3mb+cewlkcmweBKZHM2JPTk0ZednFSpVZMtycjkbLa"
160+
+ "\\ndYOS8V85AgMBewECggEBAKksaldajfDZDV6nGqbFjMiizAKJolr/M3OQw16K6o3/\\n0S31xIe3sSlgW0+UbYlF"
161+
+ "4U8KifhManD1apVSC3csafaspP4RZUHFhtBywLO9pR5c\\nr6S5aLp+gPWFyIp1pfXbWGvc5VY/v9x7ya1VEa6rXvL"
162+
+ "sKupSeWAW4tMj3eo/64ge\\nsdaceaLYw52KeBYiT6+vpsnYrEkAHO1fF/LavbLLOFJmFTMxmsNaG0tuiJHgjshB\\"
163+
+ "n82DpMCbXG9YcCgI/DbzuIjsdj2JC1cascSP//3PmefWysucBQe7Jryb6NQtASmnv\\nCdDw/0jmZTEjpe4S1lxfHp"
164+
+ "lAhHFtdgYTvyYtaLZiVVkCgYEA8eVpof2rceecw/I6\\n5ng1q3Hl2usdWV/4mZMvR0fOemacLLfocX6IYxT1zA1FF"
165+
+ "JlbXSRsJMf/Qq39mOR2\\nSpW+hr4jCoHeRVYLgsbggtrevGmILAlNoqCMpGZ6vDmJpq6ECV9olliDvpPgWOP+\\nm"
166+
+ "YPDreFBGxWvQrADNbRt2dmGsrsCgYEAyUHqB2wvJHFqdmeBsaacewzV8x9WgmeX\\ngUIi9REwXlGDW0Mz50dxpxcK"
167+
+ "CAYn65+7TCnY5O/jmL0VRxU1J2mSWyWTo1C+17L0\\n3fUqjxL1pkefwecxwecvC+gFFYdJ4CQ/MHHXU81Lwl1iWdF"
168+
+ "Cd2UoGddYaOF+KNeM\\nHC7cmqra+JsCgYEAlUNywzq8nUg7282E+uICfCB0LfwejuymR93CtsFgb7cRd6ak\\nECR"
169+
+ "8FGfCpH8ruWJINllbQfcHVCX47ndLZwqv3oVFKh6pAS/vVI4dpOepP8++7y1u\\ncoOvtreXCX6XqfrWDtKIvv0vjl"
170+
+ "HBhhhp6mCcRpdQjV38H7JsyJ7lih/oNjECgYAt\\nkndj5uNl5SiuVxHFhcZJO+XWf6ofLUregtevZakGMn8EE1uVa"
171+
+ "2AY7eafmoU/nZPT\\n00YB0TBATdCbn/nBSuKDESkhSg9s2GEKQZG5hBmL5uCMfo09z3SfxZIhJdlerreP\\nJ7gSi"
172+
+ "dI12N+EZxYd4xIJh/HFDgp7RRO87f+WJkofMQKBgGTnClK1VMaCRbJZPriw\\nEfeFCoOX75MxKwXs6xgrw4W//AYG"
173+
+ "GUjDt83lD6AZP6tws7gJ2IwY/qP7+lyhjEqN\\nHtfPZRGFkGZsdaksdlaksd323423d+15/UvrlRSFPNj1tWQmNKk"
174+
+ "XyRDW4IG1Oa2p\\nrALStNBx5Y9t0/LQnFI4w3aG\\n-----END PRIVATE KEY-----\\n\",\n"
175+
+ " \"client_email\": \"[email protected]\",\n"
176+
+ " \"client_id\": \"someclientid.apps.googleusercontent.com\",\n"
177+
+ " \"type\": \"service_account\",\n"
178+
+ " \"universe_domain\": \"random.com\"\n"
144179
+ "}";
145180

146181
static {
147182
credentials = loadCredentials(JSON_KEY);
148183
credentialsWithProjectId = loadCredentials(JSON_KEY_PROJECT_ID);
149184
credentialsWithQuotaProject = loadCredentials(JSON_KEY_QUOTA_PROJECT_ID);
185+
credentialsNotInGDU = loadCredentials(JSON_KEY_NON_GDU);
150186
}
151187

152188
static GoogleCredentials loadCredentials(String credentialFile) {
@@ -471,6 +507,129 @@ public void testResponseHeaderDoesNotContainMetaDataFlavor() throws Exception {
471507
assertThat(ServiceOptions.headerContainsMetadataFlavor(httpResponse)).isFalse();
472508
}
473509

510+
@Test
511+
public void testGetResolvedEndpoint_noUniverseDomain() {
512+
TestServiceOptions options = TestServiceOptions.newBuilder().setProjectId("project-id").build();
513+
assertThat(options.getResolvedHost("service")).isEqualTo("https://service.googleapis.com");
514+
}
515+
516+
@Test
517+
public void testGetResolvedEndpoint_emptyUniverseDomain() {
518+
TestServiceOptions options =
519+
TestServiceOptions.newBuilder().setUniverseDomain("").setProjectId("project-id").build();
520+
IllegalArgumentException exception =
521+
assertThrows(IllegalArgumentException.class, () -> options.getResolvedHost("service"));
522+
assertThat(exception.getMessage()).isEqualTo("The universe domain cannot be empty");
523+
}
524+
525+
@Test
526+
public void testGetResolvedEndpoint_customUniverseDomain() {
527+
TestServiceOptions options =
528+
TestServiceOptions.newBuilder()
529+
.setUniverseDomain("test.com")
530+
.setProjectId("project-id")
531+
.build();
532+
assertThat(options.getResolvedHost("service")).isEqualTo("https://service.test.com");
533+
}
534+
535+
@Test
536+
public void testGetResolvedEndpoint_customUniverseDomain_customHost() {
537+
TestServiceOptions options =
538+
TestServiceOptions.newBuilder()
539+
.setUniverseDomain("test.com")
540+
.setHost("https://service.random.com/")
541+
.setProjectId("project-id")
542+
.build();
543+
assertThat(options.getResolvedHost("service")).isEqualTo("https://service.random.com/");
544+
}
545+
546+
@Test
547+
public void testGetResolvedApiaryHost_noUniverseDomain() {
548+
TestServiceOptions options = TestServiceOptions.newBuilder().setProjectId("project-id").build();
549+
assertThat(options.getResolvedApiaryHost("service"))
550+
.isEqualTo("https://service.googleapis.com/");
551+
}
552+
553+
@Test
554+
public void testGetResolvedApiaryHost_customUniverseDomain_noHost() {
555+
TestServiceOptions options =
556+
TestServiceOptions.newBuilder()
557+
.setUniverseDomain("test.com")
558+
.setHost(null)
559+
.setProjectId("project-id")
560+
.build();
561+
assertThat(options.getResolvedApiaryHost("service")).isEqualTo("https://service.test.com/");
562+
}
563+
564+
@Test
565+
public void testGetResolvedApiaryHost_customUniverseDomain_customHost() {
566+
TestServiceOptions options =
567+
TestServiceOptions.newBuilder()
568+
.setUniverseDomain("test.com")
569+
.setHost("https://service.random.com")
570+
.setProjectId("project-id")
571+
.build();
572+
assertThat(options.getResolvedApiaryHost("service")).isEqualTo("https://service.test.com/");
573+
}
574+
575+
// No User Configuration = GDU, Default Credentials = GDU
576+
@Test
577+
public void testIsValidUniverseDomain_noUserUniverseDomainConfig_defaultCredentials()
578+
throws IOException {
579+
TestServiceOptions options =
580+
TestServiceOptions.newBuilder()
581+
.setProjectId("project-id")
582+
.setHost("https://test.random.com")
583+
.setCredentials(credentials)
584+
.build();
585+
assertThat(options.hasValidUniverseDomain()).isTrue();
586+
}
587+
588+
// No User Configuration = GDU, non Default Credentials = random.com
589+
// non-GDU Credentials could be any domain, the tests use random.com
590+
@Test
591+
public void testIsValidUniverseDomain_noUserUniverseDomainConfig_nonGDUCredentials()
592+
throws IOException {
593+
TestServiceOptions options =
594+
TestServiceOptions.newBuilder()
595+
.setProjectId("project-id")
596+
.setHost("https://test.random.com")
597+
.setCredentials(credentialsNotInGDU)
598+
.build();
599+
assertThat(options.hasValidUniverseDomain()).isFalse();
600+
}
601+
602+
// User Configuration = random.com, Default Credentials = GDU
603+
// User Credentials could be set to any domain, the tests use random.com
604+
@Test
605+
public void testIsValidUniverseDomain_userUniverseDomainConfig_defaultCredentials()
606+
throws IOException {
607+
TestServiceOptions options =
608+
TestServiceOptions.newBuilder()
609+
.setProjectId("project-id")
610+
.setHost("https://test.random.com")
611+
.setUniverseDomain("random.com")
612+
.setCredentials(credentials)
613+
.build();
614+
assertThat(options.hasValidUniverseDomain()).isFalse();
615+
}
616+
617+
// User Configuration = random.com, non Default Credentials = random.com
618+
// User Credentials and non GDU Credentials could be set to any domain,
619+
// the tests use random.com
620+
@Test
621+
public void testIsValidUniverseDomain_userUniverseDomainConfig_nonGDUCredentials()
622+
throws IOException {
623+
TestServiceOptions options =
624+
TestServiceOptions.newBuilder()
625+
.setProjectId("project-id")
626+
.setHost("https://test.random.com")
627+
.setUniverseDomain("random.com")
628+
.setCredentials(credentialsNotInGDU)
629+
.build();
630+
assertThat(options.hasValidUniverseDomain()).isTrue();
631+
}
632+
474633
private HttpResponse createHttpResponseWithHeader(final Multimap<String, String> headers)
475634
throws Exception {
476635
HttpTransport mockHttpTransport =

0 commit comments

Comments
 (0)