Skip to content

Commit

Permalink
feat: Add Universe Domain to Java-Core (#2329)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
lqiu96 authored Jan 4, 2024
1 parent 4175adb commit 586ac9f
Show file tree
Hide file tree
Showing 2 changed files with 253 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ public abstract class ServiceOptions<
implements Serializable {

public static final String CREDENTIAL_ENV_NAME = "GOOGLE_APPLICATION_CREDENTIALS";

private static final String DEFAULT_HOST = "https://www.googleapis.com";
private static final String LEGACY_PROJECT_ENV_NAME = "GCLOUD_PROJECT";
private static final String PROJECT_ENV_NAME = "GOOGLE_CLOUD_PROJECT";
Expand All @@ -95,6 +94,7 @@ public abstract class ServiceOptions<
protected final String clientLibToken;

private final String projectId;
private final String universeDomain;
private final String host;
private final RetrySettings retrySettings;
private final String serviceRpcFactoryClassName;
Expand Down Expand Up @@ -125,6 +125,7 @@ public abstract static class Builder<
private final ImmutableSet<String> allowedClientLibTokens =
ImmutableSet.of(ServiceOptions.getGoogApiClientLibName());
private String projectId;
private String universeDomain;
private String host;
protected Credentials credentials;
private RetrySettings retrySettings;
Expand All @@ -142,6 +143,7 @@ protected Builder() {}
@InternalApi("This class should only be extended within google-cloud-java")
protected Builder(ServiceOptions<ServiceT, OptionsT> options) {
projectId = options.projectId;
universeDomain = options.universeDomain;
host = options.host;
credentials = options.credentials;
retrySettings = options.retrySettings;
Expand Down Expand Up @@ -199,6 +201,22 @@ public B setHost(String host) {
return self();
}

/**
* Universe Domain is the domain for Google Cloud Services. A Google Cloud endpoint follows the
* format of `{ServiceName}.{UniverseDomain}`. For example, speech.googleapis.com would have a
* Universe Domain value of `googleapis.com` and cloudasset.test.com would have a Universe
* Domain of `test.com`.
*
* <p>If this value is not set, the resolved UniverseDomain will default to `googleapis.com`.
*
* @throws NullPointerException if {@code universeDomain} is {@code null}. The resolved
* universeDomain will be `googleapis.com` if this value is not set.
*/
public B setUniverseDomain(String universeDomain) {
this.universeDomain = checkNotNull(universeDomain);
return self();
}

/**
* Sets the service authentication credentials. If no credentials are set, {@link
* GoogleCredentials#getApplicationDefault()} will be used to attempt getting credentials from
Expand Down Expand Up @@ -306,6 +324,7 @@ protected ServiceOptions(
"A project ID is required for this service but could not be determined from the builder "
+ "or the environment. Please set a project ID using the builder.");
}
universeDomain = builder.universeDomain;
host = firstNonNull(builder.host, getDefaultHost());
credentials = builder.credentials != null ? builder.credentials : defaultCredentials();
retrySettings = firstNonNull(builder.retrySettings, getDefaultRetrySettings());
Expand Down Expand Up @@ -582,6 +601,19 @@ public String getProjectId() {
return projectId;
}

/**
* Universe Domain is the domain for Google Cloud Services. A Google Cloud endpoint follows the
* format of `{ServiceName}.{UniverseDomain}`. For example, speech.googleapis.com would have a
* Universe Domain value of `googleapis.com` and cloudasset.test.com would have a Universe Domain
* of `test.com`.
*
* @return The universe domain value set in the Builder's setter. This is not the resolved
* Universe Domain
*/
public String getUniverseDomain() {
return universeDomain;
}

/** Returns the service host. */
public String getHost() {
return host;
Expand Down Expand Up @@ -767,4 +799,62 @@ public String getClientLibToken() {
public String getQuotaProjectId() {
return quotaProjectId;
}

/**
* Returns the resolved host for the Service to connect to Google Cloud
*
* <p>The resolved host will be in `https://{serviceName}.{resolvedUniverseDomain}` format. The
* resolvedUniverseDomain will be set to `googleapis.com` if universeDomain is null. The format is
* similar to the DEFAULT_HOST value in java-core.
*
* @see <a
* 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>
*/
@InternalApi
public String getResolvedHost(String serviceName) {
if (universeDomain != null && universeDomain.isEmpty()) {
throw new IllegalArgumentException("The universe domain cannot be empty");
}
String resolvedUniverseDomain =
universeDomain != null ? universeDomain : Credentials.GOOGLE_DEFAULT_UNIVERSE;
// The host value set to DEFAULT_HOST if the user didn't configure a host. If the
// user set a host the library uses that value, otherwise, construct the host for the user.
// The DEFAULT_HOST value is not a valid host for handwritten libraries and should be
// overriden to include the serviceName.
if (!DEFAULT_HOST.equals(host)) {
return host;
}
return "https://" + serviceName + "." + resolvedUniverseDomain;
}

/**
* Temporarily used for BigQuery and Storage Apiary Wrapped Libraries. To be removed in the future
* when Apiary clients can resolve their endpoints. Returns the host to be used as the rootUrl.
*
* <p>The resolved host will be in `https://{serviceName}.{resolvedUniverseDomain}/` format. The
* resolvedUniverseDomain will be set to `googleapis.com` if universeDomain is null.
*
* @see <a
* 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>
*/
@InternalApi
public String getResolvedApiaryHost(String serviceName) {
String resolvedUniverseDomain =
universeDomain != null ? universeDomain : Credentials.GOOGLE_DEFAULT_UNIVERSE;
return "https://" + serviceName + "." + resolvedUniverseDomain + "/";
}

/**
* Validates that Credentials' Universe Domain matches the resolved Universe Domain. Currently,
* this is only intended for BigQuery and Storage Apiary Wrapped Libraries.
*
* <p>This validation call should be made prior to any RPC invocation. This call is used to gate
* the RPC invocation if there is no valid universe domain.
*/
@InternalApi
public boolean hasValidUniverseDomain() throws IOException {
String resolvedUniverseDomain =
universeDomain != null ? universeDomain : Credentials.GOOGLE_DEFAULT_UNIVERSE;
return resolvedUniverseDomain.equals(getCredentials().getUniverseDomain());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

Expand Down Expand Up @@ -55,6 +56,7 @@ public class ServiceOptionsTest {
private static GoogleCredentials credentials;
private static GoogleCredentials credentialsWithProjectId;
private static GoogleCredentials credentialsWithQuotaProject;
private static GoogleCredentials credentialsNotInGDU;

private static final String JSON_KEY =
"{\n"
Expand All @@ -81,7 +83,8 @@ public class ServiceOptionsTest {
+ "XyRDW4IG1Oa2p\\nrALStNBx5Y9t0/LQnFI4w3aG\\n-----END PRIVATE KEY-----\\n\",\n"
+ " \"client_email\": \"someclientid@developer.gserviceaccount.com\",\n"
+ " \"client_id\": \"someclientid.apps.googleusercontent.com\",\n"
+ " \"type\": \"service_account\"\n"
+ " \"type\": \"service_account\",\n"
+ " \"universe_domain\": \"googleapis.com\"\n"
+ "}";

private static final String JSON_KEY_PROJECT_ID =
Expand Down Expand Up @@ -110,7 +113,8 @@ public class ServiceOptionsTest {
+ " \"project_id\": \"someprojectid\",\n"
+ " \"client_email\": \"someclientid@developer.gserviceaccount.com\",\n"
+ " \"client_id\": \"someclientid.apps.googleusercontent.com\",\n"
+ " \"type\": \"service_account\"\n"
+ " \"type\": \"service_account\",\n"
+ " \"universe_domain\": \"googleapis.com\"\n"
+ "}";

private static final String JSON_KEY_QUOTA_PROJECT_ID =
Expand Down Expand Up @@ -140,13 +144,45 @@ public class ServiceOptionsTest {
+ " \"client_email\": \"someclientid@developer.gserviceaccount.com\",\n"
+ " \"client_id\": \"someclientid.apps.googleusercontent.com\",\n"
+ " \"type\": \"service_account\",\n"
+ " \"quota_project_id\": \"some-quota-project-id\"\n"
+ " \"quota_project_id\": \"some-quota-project-id\",\n"
+ " \"universe_domain\": \"googleapis.com\"\n"
+ "}";

// Key added by copying the keys above and adding in the universe domain field
private static final String JSON_KEY_NON_GDU =
"{\n"
+ " \"private_key_id\": \"somekeyid\",\n"
+ " \"private_key\": \"-----BEGIN PRIVATE KEY-----\\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggS"
+ "kAgEAAoIBAQC+K2hSuFpAdrJI\\nnCgcDz2M7t7bjdlsadsasad+fvRSW6TjNQZ3p5LLQY1kSZRqBqylRkzteMOyHg"
+ "aR\\n0Pmxh3ILCND5men43j3h4eDbrhQBuxfEMalkG92sL+PNQSETY2tnvXryOvmBRwa/\\nQP/9dJfIkIDJ9Fw9N4"
+ "Bhhhp6mCcRpdQjV38H7JsyJ7lih/oNjECgYAt\\nknddadwkwewcVxHFhcZJO+XWf6ofLUXpRwiTZakGMn8EE1uVa2"
+ "LgczOjwWHGi99MFjxSer5m9\\n1tCa3/KEGKiS/YL71JvjwX3mb+cewlkcmweBKZHM2JPTk0ZednFSpVZMtycjkbLa"
+ "\\ndYOS8V85AgMBewECggEBAKksaldajfDZDV6nGqbFjMiizAKJolr/M3OQw16K6o3/\\n0S31xIe3sSlgW0+UbYlF"
+ "4U8KifhManD1apVSC3csafaspP4RZUHFhtBywLO9pR5c\\nr6S5aLp+gPWFyIp1pfXbWGvc5VY/v9x7ya1VEa6rXvL"
+ "sKupSeWAW4tMj3eo/64ge\\nsdaceaLYw52KeBYiT6+vpsnYrEkAHO1fF/LavbLLOFJmFTMxmsNaG0tuiJHgjshB\\"
+ "n82DpMCbXG9YcCgI/DbzuIjsdj2JC1cascSP//3PmefWysucBQe7Jryb6NQtASmnv\\nCdDw/0jmZTEjpe4S1lxfHp"
+ "lAhHFtdgYTvyYtaLZiVVkCgYEA8eVpof2rceecw/I6\\n5ng1q3Hl2usdWV/4mZMvR0fOemacLLfocX6IYxT1zA1FF"
+ "JlbXSRsJMf/Qq39mOR2\\nSpW+hr4jCoHeRVYLgsbggtrevGmILAlNoqCMpGZ6vDmJpq6ECV9olliDvpPgWOP+\\nm"
+ "YPDreFBGxWvQrADNbRt2dmGsrsCgYEAyUHqB2wvJHFqdmeBsaacewzV8x9WgmeX\\ngUIi9REwXlGDW0Mz50dxpxcK"
+ "CAYn65+7TCnY5O/jmL0VRxU1J2mSWyWTo1C+17L0\\n3fUqjxL1pkefwecxwecvC+gFFYdJ4CQ/MHHXU81Lwl1iWdF"
+ "Cd2UoGddYaOF+KNeM\\nHC7cmqra+JsCgYEAlUNywzq8nUg7282E+uICfCB0LfwejuymR93CtsFgb7cRd6ak\\nECR"
+ "8FGfCpH8ruWJINllbQfcHVCX47ndLZwqv3oVFKh6pAS/vVI4dpOepP8++7y1u\\ncoOvtreXCX6XqfrWDtKIvv0vjl"
+ "HBhhhp6mCcRpdQjV38H7JsyJ7lih/oNjECgYAt\\nkndj5uNl5SiuVxHFhcZJO+XWf6ofLUregtevZakGMn8EE1uVa"
+ "2AY7eafmoU/nZPT\\n00YB0TBATdCbn/nBSuKDESkhSg9s2GEKQZG5hBmL5uCMfo09z3SfxZIhJdlerreP\\nJ7gSi"
+ "dI12N+EZxYd4xIJh/HFDgp7RRO87f+WJkofMQKBgGTnClK1VMaCRbJZPriw\\nEfeFCoOX75MxKwXs6xgrw4W//AYG"
+ "GUjDt83lD6AZP6tws7gJ2IwY/qP7+lyhjEqN\\nHtfPZRGFkGZsdaksdlaksd323423d+15/UvrlRSFPNj1tWQmNKk"
+ "XyRDW4IG1Oa2p\\nrALStNBx5Y9t0/LQnFI4w3aG\\n-----END PRIVATE KEY-----\\n\",\n"
+ " \"client_email\": \"someclientid@developer.gserviceaccount.com\",\n"
+ " \"client_id\": \"someclientid.apps.googleusercontent.com\",\n"
+ " \"type\": \"service_account\",\n"
+ " \"universe_domain\": \"random.com\"\n"
+ "}";

static {
credentials = loadCredentials(JSON_KEY);
credentialsWithProjectId = loadCredentials(JSON_KEY_PROJECT_ID);
credentialsWithQuotaProject = loadCredentials(JSON_KEY_QUOTA_PROJECT_ID);
credentialsNotInGDU = loadCredentials(JSON_KEY_NON_GDU);
}

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

@Test
public void testGetResolvedEndpoint_noUniverseDomain() {
TestServiceOptions options = TestServiceOptions.newBuilder().setProjectId("project-id").build();
assertThat(options.getResolvedHost("service")).isEqualTo("https://service.googleapis.com");
}

@Test
public void testGetResolvedEndpoint_emptyUniverseDomain() {
TestServiceOptions options =
TestServiceOptions.newBuilder().setUniverseDomain("").setProjectId("project-id").build();
IllegalArgumentException exception =
assertThrows(IllegalArgumentException.class, () -> options.getResolvedHost("service"));
assertThat(exception.getMessage()).isEqualTo("The universe domain cannot be empty");
}

@Test
public void testGetResolvedEndpoint_customUniverseDomain() {
TestServiceOptions options =
TestServiceOptions.newBuilder()
.setUniverseDomain("test.com")
.setProjectId("project-id")
.build();
assertThat(options.getResolvedHost("service")).isEqualTo("https://service.test.com");
}

@Test
public void testGetResolvedEndpoint_customUniverseDomain_customHost() {
TestServiceOptions options =
TestServiceOptions.newBuilder()
.setUniverseDomain("test.com")
.setHost("https://service.random.com/")
.setProjectId("project-id")
.build();
assertThat(options.getResolvedHost("service")).isEqualTo("https://service.random.com/");
}

@Test
public void testGetResolvedApiaryHost_noUniverseDomain() {
TestServiceOptions options = TestServiceOptions.newBuilder().setProjectId("project-id").build();
assertThat(options.getResolvedApiaryHost("service"))
.isEqualTo("https://service.googleapis.com/");
}

@Test
public void testGetResolvedApiaryHost_customUniverseDomain_noHost() {
TestServiceOptions options =
TestServiceOptions.newBuilder()
.setUniverseDomain("test.com")
.setHost(null)
.setProjectId("project-id")
.build();
assertThat(options.getResolvedApiaryHost("service")).isEqualTo("https://service.test.com/");
}

@Test
public void testGetResolvedApiaryHost_customUniverseDomain_customHost() {
TestServiceOptions options =
TestServiceOptions.newBuilder()
.setUniverseDomain("test.com")
.setHost("https://service.random.com")
.setProjectId("project-id")
.build();
assertThat(options.getResolvedApiaryHost("service")).isEqualTo("https://service.test.com/");
}

// No User Configuration = GDU, Default Credentials = GDU
@Test
public void testIsValidUniverseDomain_noUserUniverseDomainConfig_defaultCredentials()
throws IOException {
TestServiceOptions options =
TestServiceOptions.newBuilder()
.setProjectId("project-id")
.setHost("https://test.random.com")
.setCredentials(credentials)
.build();
assertThat(options.hasValidUniverseDomain()).isTrue();
}

// No User Configuration = GDU, non Default Credentials = random.com
// non-GDU Credentials could be any domain, the tests use random.com
@Test
public void testIsValidUniverseDomain_noUserUniverseDomainConfig_nonGDUCredentials()
throws IOException {
TestServiceOptions options =
TestServiceOptions.newBuilder()
.setProjectId("project-id")
.setHost("https://test.random.com")
.setCredentials(credentialsNotInGDU)
.build();
assertThat(options.hasValidUniverseDomain()).isFalse();
}

// User Configuration = random.com, Default Credentials = GDU
// User Credentials could be set to any domain, the tests use random.com
@Test
public void testIsValidUniverseDomain_userUniverseDomainConfig_defaultCredentials()
throws IOException {
TestServiceOptions options =
TestServiceOptions.newBuilder()
.setProjectId("project-id")
.setHost("https://test.random.com")
.setUniverseDomain("random.com")
.setCredentials(credentials)
.build();
assertThat(options.hasValidUniverseDomain()).isFalse();
}

// User Configuration = random.com, non Default Credentials = random.com
// User Credentials and non GDU Credentials could be set to any domain,
// the tests use random.com
@Test
public void testIsValidUniverseDomain_userUniverseDomainConfig_nonGDUCredentials()
throws IOException {
TestServiceOptions options =
TestServiceOptions.newBuilder()
.setProjectId("project-id")
.setHost("https://test.random.com")
.setUniverseDomain("random.com")
.setCredentials(credentialsNotInGDU)
.build();
assertThat(options.hasValidUniverseDomain()).isTrue();
}

private HttpResponse createHttpResponseWithHeader(final Multimap<String, String> headers)
throws Exception {
HttpTransport mockHttpTransport =
Expand Down

0 comments on commit 586ac9f

Please sign in to comment.