Skip to content

Commit

Permalink
feat: Full endpoint resolution
Browse files Browse the repository at this point in the history
  • Loading branch information
lqiu96 committed Dec 20, 2023
1 parent 8822f3b commit bdbaab1
Show file tree
Hide file tree
Showing 9 changed files with 435 additions and 41 deletions.
Expand Up @@ -295,6 +295,10 @@ private void logDirectPathMisconfig() {
Level.WARNING,
"DirectPath is misconfigured. DirectPath is only available in a GCE environment.");
}
if (!canUseDirectPathWithUniverseDomain()) {
LOG.log(
Level.WARNING, "DirectPath will only work in the the googleapis.com Universe Domain");
}
}
}
}
Expand Down Expand Up @@ -325,6 +329,10 @@ static boolean isOnComputeEngine() {
return false;
}

private boolean canUseDirectPathWithUniverseDomain() {
return endpoint.contains("googleapis.com");
}

@VisibleForTesting
ChannelCredentials createMtlsChannelCredentials() throws IOException, GeneralSecurityException {
if (mtlsProvider.useMtlsClientCertificate()) {
Expand Down Expand Up @@ -356,7 +364,10 @@ private ManagedChannel createSingleChannel() throws IOException {

// Check DirectPath traffic.
boolean useDirectPathXds = false;
if (isDirectPathEnabled() && isNonDefaultServiceAccountAllowed() && isOnComputeEngine()) {
if (isDirectPathEnabled()
&& isNonDefaultServiceAccountAllowed()
&& isOnComputeEngine()
&& canUseDirectPathWithUniverseDomain()) {
CallCredentials callCreds = MoreCallCredentials.from(credentials);
ChannelCredentials channelCreds =
GoogleDefaultChannelCredentials.newBuilder().callCredentials(callCreds).build();
Expand Down
Expand Up @@ -290,6 +290,7 @@ public void testDirectPathXdsEnabled() throws IOException {
InstantiatingGrpcChannelProvider.newBuilder()
.setAttemptDirectPath(true)
.setAttemptDirectPathXds()
.setEndpoint("test.googleapis.com:443")
.build();

assertThat(provider.isDirectPathXdsEnabled()).isTrue();
Expand Down Expand Up @@ -528,6 +529,7 @@ public void testLogDirectPathMisconfigWrongCredential() {
InstantiatingGrpcChannelProvider.newBuilder()
.setAttemptDirectPathXds()
.setAttemptDirectPath(true)
.setEndpoint("test.googleapis.com:443")
.build();
assertThat(logHandler.getAllMessages())
.contains(
Expand All @@ -545,6 +547,7 @@ public void testLogDirectPathMisconfigNotOnGCE() {
.setAttemptDirectPathXds()
.setAttemptDirectPath(true)
.setAllowNonDefaultServiceAccount(true)
.setEndpoint("test.googleapis.com:443")
.build();
if (!InstantiatingGrpcChannelProvider.isOnComputeEngine()) {
assertThat(logHandler.getAllMessages())
Expand All @@ -554,6 +557,22 @@ public void testLogDirectPathMisconfigNotOnGCE() {
InstantiatingGrpcChannelProvider.LOG.removeHandler(logHandler);
}

@Test
public void testLogDirectPathMisconfigNotInGDU() {
FakeLogHandler logHandler = new FakeLogHandler();
InstantiatingGrpcChannelProvider.LOG.addHandler(logHandler);
InstantiatingGrpcChannelProvider provider =
InstantiatingGrpcChannelProvider.newBuilder()
.setAttemptDirectPathXds()
.setAttemptDirectPath(true)
.setAllowNonDefaultServiceAccount(true)
.setEndpoint("test.random.endpoint.com:443")
.build();
assertThat(logHandler.getAllMessages())
.contains("DirectPath will only work in the the googleapis.com Universe Domain");
InstantiatingGrpcChannelProvider.LOG.removeHandler(logHandler);
}

private static class FakeLogHandler extends Handler {
List<LogRecord> records = new ArrayList<>();

Expand Down
10 changes: 10 additions & 0 deletions gax-java/gax/clirr-ignored-differences.xml
Expand Up @@ -25,4 +25,14 @@
<className>com/google/api/gax/rpc/TransportChannelProvider</className>
<method>* getEndpoint()</method>
</difference>
<difference>
<differenceType>7012</differenceType>
<className>com/google/api/gax/rpc/TransportChannelProvider</className>
<method>* needsResolvedEndpoint()</method>
<!-- Add Universe Domain to ClientContext -->
<difference>
<differenceType>7013</differenceType>
<className>com/google/api/gax/rpc/ClientContext*</className>
<method>* *UniverseDomain*(*)</method>
</difference>
</differences>
Expand Up @@ -104,6 +104,9 @@ public abstract class ClientContext {
@Nullable
abstract String getServiceName();

@Nullable
public abstract String getUniverseDomain();

@Nullable
public abstract String getEndpoint();

Expand Down Expand Up @@ -157,15 +160,29 @@ public static ClientContext create(StubSettings settings) throws IOException {
final ScheduledExecutorService backgroundExecutor = backgroundExecutorProvider.getExecutor();

Credentials credentials = settings.getCredentialsProvider().getCredentials();
boolean usingGDCH = credentials instanceof GdchCredentials;
EndpointContext endpointContext =
EndpointContext.newBuilder()
.setServiceName(settings.getServiceName())
.setUniverseDomain(settings.getUniverseDomain())
.setClientSettingsEndpoint(settings.getEndpoint())
.setTransportChannelProviderEndpoint(
settings.getTransportChannelProvider().getEndpoint())
.setMtlsEndpoint(settings.getMtlsEndpoint())
.setSwitchToMtlsEndpointAllowed(settings.getSwitchToMtlsEndpointAllowed())
.setUsingGDCH(usingGDCH)
.build();
String endpoint = endpointContext.getResolvedEndpoint();
String universeDomain = endpointContext.getResolvedUniverseDomain();

String settingsGdchApiAudience = settings.getGdchApiAudience();
if (credentials instanceof GdchCredentials) {
if (usingGDCH) {
// We recompute the GdchCredentials with the audience
String audienceString;
if (!Strings.isNullOrEmpty(settingsGdchApiAudience)) {
audienceString = settingsGdchApiAudience;
} else if (!Strings.isNullOrEmpty(settings.getEndpoint())) {
audienceString = settings.getEndpoint();
} else if (!Strings.isNullOrEmpty(endpoint)) {
audienceString = endpoint;
} else {
throw new IllegalArgumentException("Could not infer GDCH api audience from settings");
}
Expand Down Expand Up @@ -204,16 +221,6 @@ public static ClientContext create(StubSettings settings) throws IOException {
if (transportChannelProvider.needsCredentials() && credentials != null) {
transportChannelProvider = transportChannelProvider.withCredentials(credentials);
}
EndpointContext endpointContext =
EndpointContext.newBuilder()
.setServiceName(settings.getServiceName())
.setClientSettingsEndpoint(settings.getEndpoint())
.setTransportChannelProviderEndpoint(
settings.getTransportChannelProvider().getEndpoint())
.setMtlsEndpoint(settings.getMtlsEndpoint())
.setSwitchToMtlsEndpointAllowed(settings.getSwitchToMtlsEndpointAllowed())
.build();
String endpoint = endpointContext.getResolvedEndpoint();
if (transportChannelProvider.needsEndpoint()) {
transportChannelProvider = transportChannelProvider.withEndpoint(endpoint);
}
Expand Down Expand Up @@ -264,6 +271,7 @@ public static ClientContext create(StubSettings settings) throws IOException {
.setClock(clock)
.setDefaultCallContext(defaultCallContext)
.setServiceName(settings.getServiceName())
.setUniverseDomain(universeDomain)
.setEndpoint(settings.getEndpoint())
.setQuotaProjectId(settings.getQuotaProjectId())
.setStreamWatchdog(watchdog)
Expand Down Expand Up @@ -332,6 +340,8 @@ public abstract static class Builder {
// Package-Private scope for internal use only. Shared between StubSettings and ClientContext
abstract Builder setServiceName(String serviceName);

public abstract Builder setUniverseDomain(String universeDomain);

public abstract Builder setEndpoint(String endpoint);

public abstract Builder setQuotaProjectId(String QuotaProjectId);
Expand Down
Expand Up @@ -33,13 +33,30 @@
import com.google.api.gax.rpc.mtls.MtlsProvider;
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import java.io.IOException;
import javax.annotation.Nullable;

/** Contains the fields required to resolve the endpoint */
/** Contains the fields required to resolve the endpoint and Universe Domain */
@InternalApi
@AutoValue
public abstract class EndpointContext {
// Default to port 443 for HTTPS. Using HTTP requires explicitly setting the endpoint
private static final String ENDPOINT_TEMPLATE = "SERVICE_NAME.UNIVERSE_DOMAIN:443";
static final String GOOGLE_DEFAULT_UNIVERSE = "googleapis.com";

/**
* ServiceName is host URI for Google Cloud Services. It follows the format of
* `{ServiceName}.googleapis.com`. For example, speech.googleapis.com would have a ServiceName of
* speech and cloudasset.googleapis.com would have a ServiceName of cloudasset.
*/
// TODO: Remove @Nullable after first release 2024 (Builder will default to "").
@Nullable
public abstract String serviceName();

@Nullable
public abstract String universeDomain();

/**
* ServiceName is host URI for Google Cloud Services. It follows the format of
* `{ServiceName}.googleapis.com`. For example, speech.googleapis.com would have a ServiceName of
Expand Down Expand Up @@ -69,28 +86,76 @@ public abstract class EndpointContext {
@Nullable
public abstract MtlsProvider mtlsProvider();

public abstract boolean usingGDCH();

public abstract Builder toBuilder();

private String resolvedEndpoint;
private String resolvedUniverseDomain;

public static Builder newBuilder() {
return new AutoValue_EndpointContext.Builder().setSwitchToMtlsEndpointAllowed(false);
return new AutoValue_EndpointContext.Builder()
.setSwitchToMtlsEndpointAllowed(false)
.setUsingGDCH(false);
}

/** Determines the fully resolved endpoint and universe domain values */
@VisibleForTesting
void determineEndpoint() throws IOException {
MtlsProvider mtlsProvider = mtlsProvider() == null ? new MtlsProvider() : mtlsProvider();
// TransportChannelProvider's endpoint will override the ClientSettings' endpoint
String customEndpoint =
transportChannelProviderEndpoint() == null
? clientSettingsEndpoint()
: transportChannelProviderEndpoint();
resolvedEndpoint =
mtlsEndpointResolver(
customEndpoint, mtlsEndpoint(), switchToMtlsEndpointAllowed(), mtlsProvider);

// GDC-H has a separate flow
if (usingGDCH()) {
determineGDCHEndpoint(customEndpoint);
return;
}
// Check for "" (empty string)
if (universeDomain() != null && universeDomain().isEmpty()) {
throw new IllegalArgumentException("The universe domain value cannot be empty.");
}
// Universe Domain defaults to the GDU if it's not provided by the user.
resolvedUniverseDomain = universeDomain() != null ? universeDomain() : GOOGLE_DEFAULT_UNIVERSE;

if (Strings.isNullOrEmpty(customEndpoint)) {
customEndpoint = buildEndpointTemplate(serviceName(), resolvedUniverseDomain);
resolvedEndpoint =
mtlsEndpointResolver(
customEndpoint, mtlsEndpoint(), switchToMtlsEndpointAllowed(), mtlsProvider);
// Check if mTLS is configured with non-GDU
if (resolvedEndpoint.equals(mtlsEndpoint())
&& !resolvedUniverseDomain.equals(GOOGLE_DEFAULT_UNIVERSE)) {
throw new IllegalArgumentException(
"mTLS is not supported in any universe other than googleapis.com");
}
} else {
resolvedEndpoint = customEndpoint;
}
}

// GDC-H has no concept of Universe Domain. Do not set the resolvedUniverseDomain value
private void determineGDCHEndpoint(String customEndpoint) {
if (universeDomain() != null) {
throw new IllegalArgumentException(
"Universe domain configuration is incompatible with GDC-H");
} else if (customEndpoint == null) {
resolvedEndpoint = buildEndpointTemplate(serviceName(), GOOGLE_DEFAULT_UNIVERSE);
} else {
resolvedEndpoint = customEndpoint;
}
}

// Construct the endpoint with the template
private String buildEndpointTemplate(String serviceName, String resolvedUniverseDomain) {
return ENDPOINT_TEMPLATE
.replace("SERVICE_NAME", serviceName)
.replace("UNIVERSE_DOMAIN", resolvedUniverseDomain);
}

// This takes in parameters because determineEndpoint()'s logic will be updated
// to pass custom values in.
// Follows https://google.aip.dev/auth/4114 for resolving the endpoint
@VisibleForTesting
String mtlsEndpointResolver(
Expand Down Expand Up @@ -123,6 +188,14 @@ public String getResolvedEndpoint() {
return resolvedEndpoint;
}

/**
* The resolved Universe Domain is the computed Universe Domain after accounting for the custom
* Universe Domain
*/
public String getResolvedUniverseDomain() {
return resolvedUniverseDomain;
}

@AutoValue.Builder
public abstract static class Builder {
/**
Expand All @@ -132,6 +205,15 @@ public abstract static class Builder {
*/
public abstract Builder setServiceName(String serviceName);

public abstract Builder setUniverseDomain(String universeDomain);

/**
* ServiceName is host URI for Google Cloud Services. It follows the format of
* `{ServiceName}.googleapis.com`. For example, speech.googleapis.com would have a ServiceName
* of speech and cloudasset.googleapis.com would have a ServiceName of cloudasset.
*/
public abstract Builder setServiceName(String serviceName);

/**
* ClientSettingsEndpoint is the endpoint value set via the ClientSettings/StubSettings classes.
*/
Expand All @@ -149,6 +231,8 @@ public abstract static class Builder {

public abstract Builder setMtlsProvider(MtlsProvider mtlsProvider);

public abstract Builder setUsingGDCH(boolean usingGDCH);

abstract EndpointContext autoBuild();

public EndpointContext build() throws IOException {
Expand Down

0 comments on commit bdbaab1

Please sign in to comment.