From c202362df0134b5ecda31bba4e9f7a6ab27a3807 Mon Sep 17 00:00:00 2001 From: BenWhitehead Date: Fri, 14 Apr 2023 14:24:46 -0400 Subject: [PATCH 1/6] fix: update grpc x-goog-user-project handling gracefulness When an instance of credentials that hasRequestMetadata() but can't refresh an IllegalStateException can be thrown. Add new tests to force failure and update handling to be graceful of this. --- .../cloud/storage/GrpcStorageOptions.java | 73 +++++++++++++++--- .../storage/it/ITStorageOptionsTest.java | 74 +++++++++++++++++++ 2 files changed, 136 insertions(+), 11 deletions(-) create mode 100644 google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageOptionsTest.java diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java index d1b0f5802c..992b8c833c 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java @@ -24,7 +24,6 @@ import com.google.api.gax.core.CredentialsProvider; import com.google.api.gax.core.FixedCredentialsProvider; import com.google.api.gax.core.GaxProperties; -import com.google.api.gax.core.NoCredentialsProvider; import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider; import com.google.api.gax.retrying.RetrySettings; import com.google.api.gax.retrying.StreamResumptionStrategy; @@ -33,6 +32,8 @@ import com.google.api.gax.rpc.StatusCode.Code; import com.google.api.gax.rpc.internal.QuotaProjectIdHidingCredentials; import com.google.auth.Credentials; +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.OAuth2Credentials; import com.google.cloud.NoCredentials; import com.google.cloud.ServiceFactory; import com.google.cloud.ServiceOptions; @@ -54,6 +55,8 @@ import io.grpc.ManagedChannelBuilder; import java.io.IOException; import java.net.URI; +import java.time.Instant; +import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Map; @@ -160,21 +163,35 @@ private Tuple> resolveSettingsAndOpts() throw Opts defaultOpts = Opts.empty(); CredentialsProvider credentialsProvider; if (credentials instanceof NoCredentials) { - credentialsProvider = NoCredentialsProvider.create(); + // com.google.api.gax.core.NoCredentialsProvider returns null as its credentials instance + // the returned value is passed blindly to gRPC credentials, which in the case of directpath + // will end up asserting non-null. + // To avoid this obscure NPE, create a factory that will return the NoCredentials instance + credentialsProvider = FixedCredentialsProvider.create(credentials); } else { boolean foundQuotaProject = false; if (credentials.hasRequestMetadata()) { - Map> requestMetadata = credentials.getRequestMetadata(uri); - for (Entry> e : requestMetadata.entrySet()) { - String key = e.getKey(); - if ("x-goog-user-project".equals(key.trim().toLowerCase(Locale.ENGLISH))) { - List value = e.getValue(); - if (!value.isEmpty()) { - foundQuotaProject = true; - defaultOpts = Opts.from(UnifiedOpts.userProject(value.get(0))); - break; + try { + Map> requestMetadata = credentials.getRequestMetadata(uri); + for (Entry> e : requestMetadata.entrySet()) { + String key = e.getKey(); + if ("x-goog-user-project".equals(key.trim().toLowerCase(Locale.ENGLISH))) { + List value = e.getValue(); + if (!value.isEmpty()) { + foundQuotaProject = true; + defaultOpts = Opts.from(UnifiedOpts.userProject(value.get(0))); + break; + } } } + } catch (IllegalStateException e) { + // This happens when an instance of OAuth2Credentials attempts to refresh its + // access token during our attempt at getting request metadata. + // This is most easily reproduced by OAuth2Credentials.create(null); + // see com.google.auth.oauth2.OAuth2Credentials.refreshAccessToken + if (!e.getMessage().startsWith("OAuth2Credentials")) { + throw e; + } } } if (foundQuotaProject) { @@ -680,4 +697,38 @@ protected StorageSettings.Builder setInternalHeaderProvider( return super.setInternalHeaderProvider(internalHeaderProvider); } } + + private static final class DirectPathFriendlyNoCredentials extends OAuth2Credentials { + private static final DirectPathFriendlyNoCredentials INSTANCE = + new DirectPathFriendlyNoCredentials(); + + private static final AccessToken TOKEN = + AccessToken.newBuilder() + .setTokenValue("") + .setExpirationTime(Date.from(Instant.EPOCH)) + .build(); + + DirectPathFriendlyNoCredentials() { + super(TOKEN, java.time.Duration.ZERO, java.time.Duration.ZERO); + } + + @Override + public boolean hasRequestMetadata() { + return false; + } + + @Override + public AccessToken refreshAccessToken() throws IOException { + return TOKEN; + } + + public static DirectPathFriendlyNoCredentials getInstance() { + return INSTANCE; + } + + /** prevent java serialization from using a new instance */ + private Object readResolve() { + return INSTANCE; + } + } } diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageOptionsTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageOptionsTest.java new file mode 100644 index 0000000000..bcb6d09f46 --- /dev/null +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageOptionsTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed 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 com.google.cloud.storage.it; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.auth.oauth2.OAuth2Credentials; +import com.google.cloud.NoCredentials; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.StorageOptions; +import org.junit.Test; + +public final class ITStorageOptionsTest { + + @Test + public void clientShouldConstructCleanly_WithNoCredentials_http() throws Exception { + StorageOptions options = + StorageOptions.http().setCredentials(NoCredentials.getInstance()).build(); + doTest(options); + } + + @Test + public void clientShouldConstructCleanly_WithNoCredentials_grpc() throws Exception { + StorageOptions options = + StorageOptions.grpc().setCredentials(NoCredentials.getInstance()).build(); + doTest(options); + } + + @Test + public void clientShouldConstructCleanly_nullAccessToken_google_http() throws Exception { + GoogleCredentials cred = GoogleCredentials.create(/* accessToken= */ null); + StorageOptions options = StorageOptions.http().setCredentials(cred).build(); + doTest(options); + } + + @Test + public void clientShouldConstructCleanly_nullAccessToken_google_grpc() throws Exception { + GoogleCredentials cred = GoogleCredentials.create(/* accessToken= */ null); + StorageOptions options = StorageOptions.grpc().setCredentials(cred).build(); + doTest(options); + } + + @Test + public void clientShouldConstructCleanly_nullAccessToken_oauth_http() throws Exception { + OAuth2Credentials cred = OAuth2Credentials.create(null); + StorageOptions options = StorageOptions.http().setCredentials(cred).build(); + doTest(options); + } + + @Test + public void clientShouldConstructCleanly_nullAccessToken_oauth_grpc() throws Exception { + OAuth2Credentials cred = OAuth2Credentials.create(null); + StorageOptions options = StorageOptions.grpc().setCredentials(cred).build(); + doTest(options); + } + + private static void doTest(StorageOptions options) throws Exception { + //noinspection EmptyTryBlock + try (Storage ignore = options.getService()) {} + } +} From 9192d943cd6183d2f1b8ceb7f00130131ff905ce Mon Sep 17 00:00:00 2001 From: BenWhitehead Date: Mon, 17 Apr 2023 16:23:49 -0400 Subject: [PATCH 2/6] test: attempt to add a test which access directpath --- .kokoro/build.sh | 10 +++++----- .../google/cloud/storage/it/ITStorageOptionsTest.java | 10 ++++++++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/.kokoro/build.sh b/.kokoro/build.sh index f105d6757d..23fd99117d 100755 --- a/.kokoro/build.sh +++ b/.kokoro/build.sh @@ -59,7 +59,7 @@ javadoc) RETURN_CODE=$? ;; integration) - mvn -B ${INTEGRATION_TEST_ARGS} \ + GOOGLE_CLOUD_ENABLE_DIRECT_PATH_XDS=true mvn -B ${INTEGRATION_TEST_ARGS} \ -ntp \ -Penable-integration-tests \ -DtrimStackTrace=false \ @@ -71,13 +71,13 @@ integration) ;; graalvm) # Run Unit and Integration Tests with Native Image - mvn -B ${INTEGRATION_TEST_ARGS} -ntp -Pnative test - RETURN_CODE=$? +# mvn -B ${INTEGRATION_TEST_ARGS} -ntp -Pnative test + RETURN_CODE=1 ;; graalvm17) # Run Unit and Integration Tests with Native Image - mvn -B ${INTEGRATION_TEST_ARGS} -ntp -Pnative test - RETURN_CODE=$? +# mvn -B ${INTEGRATION_TEST_ARGS} -ntp -Pnative test + RETURN_CODE=1 ;; samples) SAMPLES_DIR=samples diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageOptionsTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageOptionsTest.java index bcb6d09f46..724cfc5963 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageOptionsTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageOptionsTest.java @@ -39,6 +39,16 @@ public void clientShouldConstructCleanly_WithNoCredentials_grpc() throws Excepti doTest(options); } + @Test + public void clientShouldConstructCleanly_WithNoCredentials_dp() throws Exception { + StorageOptions options = + StorageOptions.grpc() + .setCredentials(NoCredentials.getInstance()) + .setAttemptDirectPath(true) + .build(); + doTest(options); + } + @Test public void clientShouldConstructCleanly_nullAccessToken_google_http() throws Exception { GoogleCredentials cred = GoogleCredentials.create(/* accessToken= */ null); From f428d9820afab43d04d052f06d832fd67fe86230 Mon Sep 17 00:00:00 2001 From: BenWhitehead Date: Mon, 17 Apr 2023 17:20:18 -0400 Subject: [PATCH 3/6] chore: remove non-null NoCredentials hackery --- .../cloud/storage/GrpcStorageOptions.java | 45 +------------------ 1 file changed, 2 insertions(+), 43 deletions(-) diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java index 992b8c833c..639f60e3a4 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java @@ -24,6 +24,7 @@ import com.google.api.gax.core.CredentialsProvider; import com.google.api.gax.core.FixedCredentialsProvider; import com.google.api.gax.core.GaxProperties; +import com.google.api.gax.core.NoCredentialsProvider; import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider; import com.google.api.gax.retrying.RetrySettings; import com.google.api.gax.retrying.StreamResumptionStrategy; @@ -32,8 +33,6 @@ import com.google.api.gax.rpc.StatusCode.Code; import com.google.api.gax.rpc.internal.QuotaProjectIdHidingCredentials; import com.google.auth.Credentials; -import com.google.auth.oauth2.AccessToken; -import com.google.auth.oauth2.OAuth2Credentials; import com.google.cloud.NoCredentials; import com.google.cloud.ServiceFactory; import com.google.cloud.ServiceOptions; @@ -55,8 +54,6 @@ import io.grpc.ManagedChannelBuilder; import java.io.IOException; import java.net.URI; -import java.time.Instant; -import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Map; @@ -163,11 +160,7 @@ private Tuple> resolveSettingsAndOpts() throw Opts defaultOpts = Opts.empty(); CredentialsProvider credentialsProvider; if (credentials instanceof NoCredentials) { - // com.google.api.gax.core.NoCredentialsProvider returns null as its credentials instance - // the returned value is passed blindly to gRPC credentials, which in the case of directpath - // will end up asserting non-null. - // To avoid this obscure NPE, create a factory that will return the NoCredentials instance - credentialsProvider = FixedCredentialsProvider.create(credentials); + credentialsProvider = NoCredentialsProvider.create(); } else { boolean foundQuotaProject = false; if (credentials.hasRequestMetadata()) { @@ -697,38 +690,4 @@ protected StorageSettings.Builder setInternalHeaderProvider( return super.setInternalHeaderProvider(internalHeaderProvider); } } - - private static final class DirectPathFriendlyNoCredentials extends OAuth2Credentials { - private static final DirectPathFriendlyNoCredentials INSTANCE = - new DirectPathFriendlyNoCredentials(); - - private static final AccessToken TOKEN = - AccessToken.newBuilder() - .setTokenValue("") - .setExpirationTime(Date.from(Instant.EPOCH)) - .build(); - - DirectPathFriendlyNoCredentials() { - super(TOKEN, java.time.Duration.ZERO, java.time.Duration.ZERO); - } - - @Override - public boolean hasRequestMetadata() { - return false; - } - - @Override - public AccessToken refreshAccessToken() throws IOException { - return TOKEN; - } - - public static DirectPathFriendlyNoCredentials getInstance() { - return INSTANCE; - } - - /** prevent java serialization from using a new instance */ - private Object readResolve() { - return INSTANCE; - } - } } From c79c04909e4ae905ba62f686ffb6605e44300b31 Mon Sep 17 00:00:00 2001 From: BenWhitehead Date: Tue, 18 Apr 2023 13:04:09 -0400 Subject: [PATCH 4/6] test: refactor ITStorageOptionsTest to be parameterized --- .../com/google/cloud/storage/TestUtils.java | 20 ++++++ .../storage/it/ITStorageOptionsTest.java | 71 +++++++++---------- 2 files changed, 55 insertions(+), 36 deletions(-) diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/TestUtils.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/TestUtils.java index 0ce0b4c31e..875a3b0d85 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/TestUtils.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/TestUtils.java @@ -35,6 +35,7 @@ import com.google.cloud.storage.Retrying.RetryingDependencies; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.google.common.io.Files; import com.google.protobuf.Any; import com.google.protobuf.ByteString; import com.google.rpc.DebugInfo; @@ -45,10 +46,12 @@ import io.grpc.netty.shaded.io.netty.buffer.ByteBufUtil; import io.grpc.netty.shaded.io.netty.buffer.Unpooled; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.nio.Buffer; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -264,4 +267,21 @@ public static void assertAll(ThrowingRunnable... trs) throws Exception { map.put(k2, v2); return Collections.unmodifiableMap(map); } + + // copied with minor modification from + // com.google.api.gax.grpc.InstantiatingGrpcChannelProvider#isOnComputeEngine + public static boolean isOnComputeEngine() { + String osName = System.getProperty("os.name"); + if ("Linux".equals(osName)) { + try { + String result = + Files.asCharSource(new File("/sys/class/dmi/id/product_name"), StandardCharsets.UTF_8) + .readFirstLine(); + return result != null && (result.contains("Google") || result.contains("Compute Engine")); + } catch (IOException ignored) { + return false; + } + } + return false; + } } diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageOptionsTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageOptionsTest.java index 724cfc5963..7e485fa8aa 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageOptionsTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageOptionsTest.java @@ -16,64 +16,63 @@ package com.google.cloud.storage.it; +import static org.junit.Assume.assumeTrue; + +import com.google.auth.Credentials; import com.google.auth.oauth2.GoogleCredentials; import com.google.auth.oauth2.OAuth2Credentials; import com.google.cloud.NoCredentials; import com.google.cloud.storage.Storage; import com.google.cloud.storage.StorageOptions; +import com.google.cloud.storage.TestUtils; +import com.google.cloud.storage.it.ITStorageOptionsTest.CredentialsParameters; +import com.google.cloud.storage.it.runner.StorageITRunner; +import com.google.cloud.storage.it.runner.annotations.Backend; +import com.google.cloud.storage.it.runner.annotations.Parameterized; +import com.google.cloud.storage.it.runner.annotations.Parameterized.Parameter; +import com.google.cloud.storage.it.runner.annotations.Parameterized.ParametersProvider; +import com.google.cloud.storage.it.runner.annotations.SingleBackend; +import com.google.common.collect.ImmutableList; import org.junit.Test; +import org.junit.runner.RunWith; +@RunWith(StorageITRunner.class) +@SingleBackend(Backend.PROD) +@Parameterized(CredentialsParameters.class) public final class ITStorageOptionsTest { - @Test - public void clientShouldConstructCleanly_WithNoCredentials_http() throws Exception { - StorageOptions options = - StorageOptions.http().setCredentials(NoCredentials.getInstance()).build(); - doTest(options); - } - - @Test - public void clientShouldConstructCleanly_WithNoCredentials_grpc() throws Exception { - StorageOptions options = - StorageOptions.grpc().setCredentials(NoCredentials.getInstance()).build(); - doTest(options); - } + public static final class CredentialsParameters implements ParametersProvider { - @Test - public void clientShouldConstructCleanly_WithNoCredentials_dp() throws Exception { - StorageOptions options = - StorageOptions.grpc() - .setCredentials(NoCredentials.getInstance()) - .setAttemptDirectPath(true) - .build(); - doTest(options); + @Override + public ImmutableList parameters() { + return ImmutableList.of( + NoCredentials.getInstance(), + GoogleCredentials.create(/* accessToken= */ null), + OAuth2Credentials.create(null)); + } } - @Test - public void clientShouldConstructCleanly_nullAccessToken_google_http() throws Exception { - GoogleCredentials cred = GoogleCredentials.create(/* accessToken= */ null); - StorageOptions options = StorageOptions.http().setCredentials(cred).build(); - doTest(options); - } + @Parameter public Credentials credentials; @Test - public void clientShouldConstructCleanly_nullAccessToken_google_grpc() throws Exception { - GoogleCredentials cred = GoogleCredentials.create(/* accessToken= */ null); - StorageOptions options = StorageOptions.grpc().setCredentials(cred).build(); + public void clientShouldConstructCleanly_http() throws Exception { + StorageOptions options = StorageOptions.http().setCredentials(credentials).build(); doTest(options); } @Test - public void clientShouldConstructCleanly_nullAccessToken_oauth_http() throws Exception { - OAuth2Credentials cred = OAuth2Credentials.create(null); - StorageOptions options = StorageOptions.http().setCredentials(cred).build(); + public void clientShouldConstructCleanly_grpc() throws Exception { + StorageOptions options = + StorageOptions.grpc().setCredentials(credentials).setAttemptDirectPath(false).build(); doTest(options); } @Test - public void clientShouldConstructCleanly_nullAccessToken_oauth_grpc() throws Exception { - OAuth2Credentials cred = OAuth2Credentials.create(null); - StorageOptions options = StorageOptions.grpc().setCredentials(cred).build(); + public void clientShouldConstructCleanly_directPath() throws Exception { + assumeTrue( + "Unable to determine environment can access directPath", TestUtils.isOnComputeEngine()); + StorageOptions options = + StorageOptions.grpc().setCredentials(credentials).setAttemptDirectPath(true).build(); doTest(options); } From a30b6b811cc0557f8929d59b50cf487eca4f8a9b Mon Sep 17 00:00:00 2001 From: BenWhitehead Date: Tue, 18 Apr 2023 17:47:15 -0400 Subject: [PATCH 5/6] chore: ignore direct path test for the time being --- .../java/com/google/cloud/storage/it/ITStorageOptionsTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageOptionsTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageOptionsTest.java index 7e485fa8aa..85ee5f642c 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageOptionsTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageOptionsTest.java @@ -33,6 +33,7 @@ import com.google.cloud.storage.it.runner.annotations.Parameterized.ParametersProvider; import com.google.cloud.storage.it.runner.annotations.SingleBackend; import com.google.common.collect.ImmutableList; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @@ -68,6 +69,7 @@ public void clientShouldConstructCleanly_grpc() throws Exception { } @Test + @Ignore("waiting on conformation from the backend team if this should even be possible") public void clientShouldConstructCleanly_directPath() throws Exception { assumeTrue( "Unable to determine environment can access directPath", TestUtils.isOnComputeEngine()); From 6c83bce4a4978abc2a9059ae14e591f4e23c4c07 Mon Sep 17 00:00:00 2001 From: BenWhitehead Date: Tue, 18 Apr 2023 17:48:45 -0400 Subject: [PATCH 6/6] chore: revert modifications to build.sh --- .kokoro/build.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.kokoro/build.sh b/.kokoro/build.sh index 23fd99117d..f105d6757d 100755 --- a/.kokoro/build.sh +++ b/.kokoro/build.sh @@ -59,7 +59,7 @@ javadoc) RETURN_CODE=$? ;; integration) - GOOGLE_CLOUD_ENABLE_DIRECT_PATH_XDS=true mvn -B ${INTEGRATION_TEST_ARGS} \ + mvn -B ${INTEGRATION_TEST_ARGS} \ -ntp \ -Penable-integration-tests \ -DtrimStackTrace=false \ @@ -71,13 +71,13 @@ integration) ;; graalvm) # Run Unit and Integration Tests with Native Image -# mvn -B ${INTEGRATION_TEST_ARGS} -ntp -Pnative test - RETURN_CODE=1 + mvn -B ${INTEGRATION_TEST_ARGS} -ntp -Pnative test + RETURN_CODE=$? ;; graalvm17) # Run Unit and Integration Tests with Native Image -# mvn -B ${INTEGRATION_TEST_ARGS} -ntp -Pnative test - RETURN_CODE=1 + mvn -B ${INTEGRATION_TEST_ARGS} -ntp -Pnative test + RETURN_CODE=$? ;; samples) SAMPLES_DIR=samples