New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Add Universe Domain Support #2435
Changes from 18 commits
b49eeca
e15caa2
4544811
3b8a90a
544ee32
2ba2504
25c9bc8
13418de
b8b233a
dfd07a7
a9a64db
0f905c0
b2088be
fd1182a
3edfe45
bba201a
7a0b3fa
83612b6
1cfd93a
3fcb3c8
8d11d10
46781b1
9c8084c
94c42d1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,6 +20,9 @@ | |
import com.google.api.client.util.ObjectParser; | ||
import com.google.api.client.util.Preconditions; | ||
import com.google.api.client.util.Strings; | ||
import com.google.auth.Credentials; | ||
import com.google.auth.http.HttpCredentialsAdapter; | ||
import com.google.common.annotations.VisibleForTesting; | ||
import java.io.IOException; | ||
import java.util.logging.Logger; | ||
|
||
|
@@ -33,6 +36,8 @@ public abstract class AbstractGoogleClient { | |
|
||
private static final Logger logger = Logger.getLogger(AbstractGoogleClient.class.getName()); | ||
|
||
private static final String GOOGLE_CLOUD_UNIVERSE_DOMAIN = "GOOGLE_CLOUD_UNIVERSE_DOMAIN"; | ||
|
||
/** The request factory for connections to the server. */ | ||
private final HttpRequestFactory requestFactory; | ||
|
||
|
@@ -68,13 +73,18 @@ public abstract class AbstractGoogleClient { | |
/** Whether discovery required parameter checks should be suppressed. */ | ||
private final boolean suppressRequiredParameterChecks; | ||
|
||
private final String universeDomain; | ||
|
||
private final HttpRequestInitializer httpRequestInitializer; | ||
|
||
/** | ||
* @param builder builder | ||
* @since 1.14 | ||
*/ | ||
protected AbstractGoogleClient(Builder builder) { | ||
googleClientRequestInitializer = builder.googleClientRequestInitializer; | ||
rootUrl = normalizeRootUrl(builder.rootUrl); | ||
universeDomain = determineUniverseDomain(builder); | ||
rootUrl = normalizeRootUrl(determineEndpoint(builder)); | ||
servicePath = normalizeServicePath(builder.servicePath); | ||
batchPath = builder.batchPath; | ||
if (Strings.isNullOrEmpty(builder.applicationName)) { | ||
|
@@ -88,6 +98,74 @@ protected AbstractGoogleClient(Builder builder) { | |
objectParser = builder.objectParser; | ||
suppressPatternChecks = builder.suppressPatternChecks; | ||
suppressRequiredParameterChecks = builder.suppressRequiredParameterChecks; | ||
httpRequestInitializer = builder.httpRequestInitializer; | ||
} | ||
|
||
/** | ||
* Resolve the Universe Domain to be used when resolving the endpoint. The logic for resolving the | ||
* universe domain is the following order: 1. Use the user configured value is set, 2. Use the | ||
* Universe Domain Env Var if set, 3. Default to the Google Default Universe | ||
*/ | ||
private String determineUniverseDomain(Builder builder) { | ||
String resolvedUniverseDomain = builder.universeDomain; | ||
if (resolvedUniverseDomain == null) { | ||
resolvedUniverseDomain = System.getenv(GOOGLE_CLOUD_UNIVERSE_DOMAIN); | ||
} | ||
return resolvedUniverseDomain == null | ||
? Credentials.GOOGLE_DEFAULT_UNIVERSE | ||
: resolvedUniverseDomain; | ||
} | ||
|
||
/** | ||
* Resolve the endpoint based on user configurations. If the user has configured a custom rootUrl, | ||
* use that value. Otherwise, construct the endpoint based on the serviceName and the | ||
* universeDomain. | ||
*/ | ||
private String determineEndpoint(Builder builder) { | ||
boolean mtlsEnabled = builder.rootUrl.contains(".mtls."); | ||
// mTLS configurations is not compatible with anything other than the GDU | ||
if (mtlsEnabled && !universeDomain.equals(Credentials.GOOGLE_DEFAULT_UNIVERSE)) { | ||
throw new IllegalArgumentException( | ||
"mTLS is not supported in any universe other than googleapis.com"); | ||
} | ||
// If the serviceName is null, we cannot construct a valid resolved endpoint. Simply return | ||
// the rootUrl as this was custom configuration passed in. | ||
if (builder.isUserConfiguredEndpoint || builder.serviceName == null) { | ||
return builder.rootUrl; | ||
} | ||
if (mtlsEnabled) { | ||
return "https://" + builder.serviceName + ".mtls." + universeDomain + "/"; | ||
} | ||
return "https://" + builder.serviceName + "." + universeDomain + "/"; | ||
} | ||
|
||
/** | ||
* Check that the User configured universe domain matches the Credentials' universe domain. This | ||
* uses the HttpRequestInitializer to get the Credentials and is enforced that the | ||
* HttpRequestInitializer is of the {@see <a | ||
* href="https://github.com/googleapis/google-auth-library-java/blob/main/oauth2_http/java/com/google/auth/http/HttpCredentialsAdapter.java">HttpCredentialsAdapter</a>} | ||
* from the google-auth-library. If the HttpRequestInitializer is not used, the configured | ||
* Universe Domain is validated against the Google Default Universe (GDU): `googleapis.com`. | ||
* | ||
* @throws IOException if the configured Universe Domain does not match the Universe Domain in the | ||
* Credentials or there is an error reading the Universe Domain from the credentials | ||
*/ | ||
public void validateUniverseDomain() throws IOException { | ||
String expectedUniverseDomain; | ||
if (!(httpRequestInitializer instanceof HttpCredentialsAdapter)) { | ||
expectedUniverseDomain = Credentials.GOOGLE_DEFAULT_UNIVERSE; | ||
} else { | ||
Credentials credentials = ((HttpCredentialsAdapter) httpRequestInitializer).getCredentials(); | ||
expectedUniverseDomain = credentials.getUniverseDomain(); | ||
} | ||
if (!expectedUniverseDomain.equals(getUniverseDomain())) { | ||
throw new IOException( | ||
String.format( | ||
"The configured universe domain (%s) does not match the universe domain found" | ||
+ " in the credentials (%s). If you haven't configured the universe domain" | ||
+ " explicitly, `googleapis.com` is the default.", | ||
getUniverseDomain(), expectedUniverseDomain)); | ||
} | ||
} | ||
|
||
/** | ||
|
@@ -139,6 +217,18 @@ public final GoogleClientRequestInitializer getGoogleClientRequestInitializer() | |
return googleClientRequestInitializer; | ||
} | ||
|
||
/** | ||
* Universe Domain is the domain for Google Cloud Services. It 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`. If this value is not set, this will default to `googleapis.com`. | ||
* | ||
* @return The configured Universe Domain or the Google Default Universe (googleapis.com) | ||
*/ | ||
public final String getUniverseDomain() { | ||
return universeDomain; | ||
} | ||
|
||
/** | ||
* Returns the object parser or {@code null} for none. | ||
* | ||
|
@@ -173,6 +263,7 @@ public ObjectParser getObjectParser() { | |
* @param httpClientRequest Google client request type | ||
*/ | ||
protected void initialize(AbstractGoogleClientRequest<?> httpClientRequest) throws IOException { | ||
validateUniverseDomain(); | ||
if (getGoogleClientRequestInitializer() != null) { | ||
getGoogleClientRequestInitializer().initialize(httpClientRequest); | ||
} | ||
|
@@ -311,6 +402,26 @@ public abstract static class Builder { | |
/** Whether discovery required parameter checks should be suppressed. */ | ||
boolean suppressRequiredParameterChecks; | ||
|
||
/** User configured Universe Domain. Defaults to `googleapis.com`. */ | ||
String universeDomain; | ||
|
||
/** | ||
* Whether the user has configured an endpoint via {@link #setRootUrl(String)}. This is added in | ||
* because the rootUrl is set in the Builder's constructor. , | ||
* | ||
* <p>Apiary clients don't allow user configurations to this Builder's constructor, so this | ||
* would be set to false by default for Apiary libraries. User configuration to the rootUrl is | ||
* done via {@link #setRootUrl(String)}. | ||
* | ||
* <p>For other uses cases that touch this Builder's constructor directly, check if the rootUrl | ||
* passed in references the Google Default Universe (GDU). Any rootUrl value that is not set in | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this logic is correct, customers could set regional endpoints which also end with GDU, see https://cloud.google.com/storage/docs/regional-endpoints There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah. Turns out I think this works, but for the wrong reasons. I believe we're parsing out the serviceName and then reconstructing it to build the the same endpoint later on. It's marked as a non-user configured endpoint when it should be. I think it might be better if I change the |
||
* the GDU is a user configured endpoint. | ||
*/ | ||
boolean isUserConfiguredEndpoint; | ||
|
||
/** The parsed serviceName value from the rootUrl from the Discovery Doc. */ | ||
String serviceName; | ||
|
||
/** | ||
* Returns an instance of a new builder. | ||
* | ||
|
@@ -328,9 +439,35 @@ protected Builder( | |
HttpRequestInitializer httpRequestInitializer) { | ||
this.transport = Preconditions.checkNotNull(transport); | ||
this.objectParser = objectParser; | ||
setRootUrl(rootUrl); | ||
setServicePath(servicePath); | ||
this.rootUrl = normalizeRootUrl(rootUrl); | ||
this.servicePath = normalizeServicePath(servicePath); | ||
this.httpRequestInitializer = httpRequestInitializer; | ||
this.serviceName = parseServiceName(rootUrl); | ||
this.isUserConfiguredEndpoint = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need to determine if the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yep, it's for the (hopefully) rare user use-case where users are directly extending this class and calling the builder. The apiary libraries don't expose the endpoint configurations in the Builder directly. |
||
!this.rootUrl.endsWith(Credentials.GOOGLE_DEFAULT_UNIVERSE + "/"); | ||
} | ||
|
||
/** | ||
* This is intended to invoked once on the initial Builder's constructor call. This parses the | ||
* rootUrl value (set from the Discovery Doc) to use for the serviceName. The serviceName is | ||
* used to construct an endpoint when the user passes in a custom Universe Domain value. | ||
* | ||
* <p>The roolUrl from the Discovery Docs will always follow the format of | ||
* https://{serviceName}(.mtls).googleapis.com/ | ||
*/ | ||
private String parseServiceName(String rootUrl) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we can use the regex to extract the service name now. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good, will update, |
||
// len of "https://" | ||
int startIndex = 8; | ||
if (rootUrl.contains(".mtls.")) { | ||
return rootUrl.substring(startIndex, rootUrl.indexOf(".mtls")); | ||
} else if (rootUrl.contains(".googleapis.com")) { | ||
return rootUrl.substring(startIndex, rootUrl.indexOf(".googleapis.com")); | ||
} else { | ||
// Return null to not break behavior for any non-google users or any use | ||
// case without a discovery doc. There may be certain use cases for this | ||
// as the Builder's constructor is only protected scope | ||
return null; | ||
} | ||
} | ||
|
||
/** Builds a new instance of {@link AbstractGoogleClient}. */ | ||
|
@@ -371,6 +508,7 @@ public final String getRootUrl() { | |
* changing the return type, but nothing else. | ||
*/ | ||
public Builder setRootUrl(String rootUrl) { | ||
this.isUserConfiguredEndpoint = true; | ||
this.rootUrl = normalizeRootUrl(rootUrl); | ||
return this; | ||
} | ||
|
@@ -515,5 +653,24 @@ public Builder setSuppressRequiredParameterChecks(boolean suppressRequiredParame | |
public Builder setSuppressAllChecks(boolean suppressAllChecks) { | ||
return setSuppressPatternChecks(true).setSuppressRequiredParameterChecks(true); | ||
} | ||
|
||
/** | ||
* Sets the user configured Universe Domain value. This value will be used to try and construct | ||
* the endpoint to connect to GCP services. | ||
* | ||
* @throws IllegalStateException if universeDomain is passed in with an empty string ("") | ||
*/ | ||
public Builder setUniverseDomain(String universeDomain) { | ||
if (universeDomain != null && universeDomain.isEmpty()) { | ||
throw new IllegalArgumentException("The universe domain value cannot be empty."); | ||
} | ||
this.universeDomain = universeDomain; | ||
return this; | ||
} | ||
|
||
@VisibleForTesting | ||
String getServiceName() { | ||
return serviceName; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is it an
IOException
? It is a comparison that does not involve IO operations, hence I think it should be a runtime exception.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah right. Will change.