Skip to content

Commit

Permalink
Do not use Google's HTTP client to get the default project ID
Browse files Browse the repository at this point in the history
As described in [this issue](quarkusio/quarkus#35500), Quarkus w/ the Google clooud services and OpenCensus shim does not startup due to a static initialization issue.

`com.google.cloud.ServiceOptions` is used by the extension to get the Google cloud default-project-ID, which uses Google's HTTP client, which uses OpenCensus, which triggers an initialization of OpenTelemetry, which conflicts with the OTel init from Quarkus.

This change updates `GcpDefaultsConfigSourceFactory` to use Java's HTTP client.

See also #487, the alternatives mentioned in [this comment](#487 (comment)) to implement a `ConfigSource` and in [this comment](#487 (comment)) to set `otel.java.global-autoconfigure.enabled=false)` end in the same OpenCensus/OpenTracing init race.

Fixes quarkusio/quarkus#35500
  • Loading branch information
snazy committed Sep 14, 2023
1 parent 235cc8d commit 8ec386e
Show file tree
Hide file tree
Showing 4 changed files with 230 additions and 24 deletions.
31 changes: 31 additions & 0 deletions common/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,28 @@
<groupId>org.graalvm.sdk</groupId>
<artifactId>graal-sdk</artifactId>
</dependency>

<!-- Tests -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-opencensus-shim</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down Expand Up @@ -97,6 +119,15 @@
</annotationProcessorPaths>
</configuration>
</plugin>
<!-- <plugin>-->
<!-- <artifactId>maven-surefire-plugin</artifactId>-->
<!-- <configuration>-->
<!-- <systemProperties>-->
<!-- <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>-->
<!-- <gcloud.test>true</gcloud.test>-->
<!-- </systemProperties>-->
<!-- </configuration>-->
<!-- </plugin>-->
</plugins>
</build>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,23 @@
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;

import org.eclipse.microprofile.config.spi.ConfigSource;

import com.google.cloud.PlatformInformation;
import com.google.cloud.ServiceDefaults;
import com.google.cloud.ServiceOptions;
import com.google.common.annotations.VisibleForTesting;

import io.smallrye.config.ConfigSourceContext;
import io.smallrye.config.ConfigSourceFactory;
Expand All @@ -17,40 +29,138 @@

public class GcpDefaultsConfigSourceFactory implements ConfigSourceFactory {

private static final String OPENTELEMETRY_CONTEXT_CONTEXT_STORAGE_PROVIDER_SYS_PROP = "io.opentelemetry.context.contextStorageProvider";
private final Supplier<String> defaultProjectIdSupplier;

public GcpDefaultsConfigSourceFactory() {
this(ServiceOptionsHelper::getDefaultProjectId);
}

@VisibleForTesting
GcpDefaultsConfigSourceFactory(Supplier<String> defaultProjectIdSupplier) {
this.defaultProjectIdSupplier = defaultProjectIdSupplier;
}

@Override
public Iterable<ConfigSource> getConfigSources(final ConfigSourceContext context) {
ConfigValue enableMetadataServer = context.getValue("quarkus.google.cloud.enable-metadata-server");
if (enableMetadataServer.getValue() != null) {
if (Converters.getImplicitConverter(Boolean.class).convert(enableMetadataServer.getValue())) {
String previousContextStorageSysProp = null;
try {
// Google HTTP Client under the hood which attempts to record traces via OpenCensus which is wired
// to delegate to OpenTelemetry.
// This can lead to problems with the Quarkus OpenTelemetry extension which expects Vert.x to be running,
// something that is not the case at build time, see https://github.com/quarkusio/quarkus/issues/35500
previousContextStorageSysProp = System.setProperty(OPENTELEMETRY_CONTEXT_CONTEXT_STORAGE_PROVIDER_SYS_PROP,
"default");

String defaultProjectId = ServiceOptions.getDefaultProjectId();
if (defaultProjectId != null) {
return singletonList(
new PropertiesConfigSource(Map.of("quarkus.google.cloud.project-id", defaultProjectId),
"GcpDefaultsConfigSource",
-Integer.MAX_VALUE));
}
} finally {
if (previousContextStorageSysProp == null) {
System.clearProperty(OPENTELEMETRY_CONTEXT_CONTEXT_STORAGE_PROVIDER_SYS_PROP);
} else {
System.setProperty(OPENTELEMETRY_CONTEXT_CONTEXT_STORAGE_PROVIDER_SYS_PROP,
previousContextStorageSysProp);
String defaultProjectId = defaultProjectIdSupplier.get();
if (defaultProjectId != null) {
return singletonList(
new PropertiesConfigSource(Map.of("quarkus.google.cloud.project-id", defaultProjectId),
"GcpDefaultsConfigSource",
-Integer.MAX_VALUE));
}
}
}
return emptyList();
}

/**
* This is a partial copy of {@link ServiceOptions} to prevent the use of Google's HTTP client, which causes
* static initialization trouble via OpenCensus-shim with OpenTelemetry.
*
* <p>
* This helper class is only intended to not use the Google HTTP client but not change any other aspects of
* how the default project ID is retrieved.
*
* <p>
* (The {@link ServiceOptions} class is licensed using ASL2.)
*/
@SuppressWarnings("rawtypes")
static class ServiceOptionsHelper extends ServiceOptions {
@SuppressWarnings("unchecked")
protected ServiceOptionsHelper(Class serviceFactoryClass, Class rpcFactoryClass, Builder builder,
ServiceDefaults serviceDefaults) {
super(serviceFactoryClass, rpcFactoryClass, builder, serviceDefaults);
throw new UnsupportedOperationException();
}

@Override
protected Set<String> getScopes() {
throw new UnsupportedOperationException();
}

@Override
public Builder toBuilder() {
throw new UnsupportedOperationException();
}

public static String getDefaultProjectId() {
// As in the original `ServiceOptions` class
String projectId = System.getProperty("GOOGLE_CLOUD_PROJECT", System.getenv("GOOGLE_CLOUD_PROJECT"));
if (projectId == null) {
projectId = System.getProperty("GCLOUD_PROJECT", System.getenv("GCLOUD_PROJECT"));
}

if (projectId == null) {
projectId = getAppEngineProjectId();
}

if (projectId == null) {
projectId = getServiceAccountProjectId();
}

return projectId != null ? projectId : getGoogleCloudProjectId();
}

protected static String getAppEngineProjectId() {
// As in the original `ServiceOptions` class
String projectId;
if (PlatformInformation.isOnGAEStandard7()) {
projectId = getAppEngineProjectIdFromAppId();
} else {
projectId = System.getenv("GOOGLE_CLOUD_PROJECT");
if (projectId == null) {
projectId = System.getenv("GCLOUD_PROJECT");
}

if (projectId == null) {
projectId = getAppEngineProjectIdFromAppId();
}

if (projectId == null) {
try {
projectId = getAppEngineProjectIdFromMetadataServer();
} catch (IOException var2) {
// projectId = null;
}
}
}

return projectId;
}

/**
* This function has been changed to use the (new) Java HTTP client.
*/
private static String getAppEngineProjectIdFromMetadataServer() throws IOException {
String metadata = "http://metadata.google.internal";
String projectIdURL = "/computeMetadata/v1/project/project-id";

try {
URI uri = new URI(metadata + projectIdURL);
HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofMillis(500)).build();
HttpRequest request = HttpRequest.newBuilder().timeout(Duration.ofMillis(500)).GET().uri(uri)
.header("Metadata-Flavor", "Google").build();
HttpResponse.BodyHandler<String> bodyHandler = HttpResponse.BodyHandlers.ofString();
HttpResponse<String> response = client.send(request, bodyHandler);
return headerContainsMetadataFlavor(response) ? response.body() : "";
} catch (URISyntaxException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException(e);
}
}
return emptyList();

/**
* This function has been adopted for the Java HTTP client.
*/
private static boolean headerContainsMetadataFlavor(HttpResponse<?> response) {
String metadataFlavorValue = response.headers().firstValue("Metadata-Flavor").orElse("");
return "Google".equals(metadataFlavorValue);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package io.quarkiverse.googlecloudservices.common;

import static org.assertj.core.api.Assertions.assertThat;

import org.eclipse.microprofile.config.spi.ConfigSource;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.OpenTelemetry;
import io.smallrye.config.ConfigSourceContext;
import io.smallrye.config.ConfigValue;

class GcpDefaultsConfigSourceFactoryTest {
@Test
void configSourceWorks() {
ConfigSourceContext context = Mockito.mock(ConfigSourceContext.class);
Mockito.when(context.getValue("quarkus.google.cloud.enable-metadata-server"))
.thenReturn(ConfigValue.builder().withValue("true").build());

Iterable<ConfigSource> configSources = new GcpDefaultsConfigSourceFactory(() -> "test-project-id")
.getConfigSources(context);
assertThat(configSources).asList().hasSize(1);

ConfigSource configSource = configSources.iterator().next();
assertThat(configSource.getProperties()).containsEntry("quarkus.google.cloud.project-id", "test-project-id");
}

@Test
void metadataServerDisabled() {
ConfigSourceContext context = Mockito.mock(ConfigSourceContext.class);
Mockito.when(context.getValue("quarkus.google.cloud.enable-metadata-server"))
.thenReturn(ConfigValue.builder().withValue("false").build());

Iterable<ConfigSource> configSources = new GcpDefaultsConfigSourceFactory(() -> "test-project-id")
.getConfigSources(context);
assertThat(configSources).isEmpty();
}

@Test
void staticOpenCensusOpenTelemetryInit() {
try {
GlobalOpenTelemetry.resetForTest();

ConfigSourceContext context = Mockito.mock(ConfigSourceContext.class);
Mockito.when(context.getValue("quarkus.google.cloud.enable-metadata-server"))
.thenReturn(ConfigValue.builder().withValue("true").build());

// Uses the "real" implementation that tries to fetch the default-project-id
Iterable<ConfigSource> configSources = new GcpDefaultsConfigSourceFactory().getConfigSources(context);
assertThat(configSources).asList().hasSizeLessThanOrEqualTo(1);

// This is a pretty ugly way, because it changes static state
GlobalOpenTelemetry.set(OpenTelemetry.noop());
} finally {
GlobalOpenTelemetry.resetForTest();
}
}
}
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<compiler-plugin.version>3.8.1</compiler-plugin.version>
<enforcer-plugin.version>3.4.1</enforcer-plugin.version>
<assertj.version>3.24.2</assertj.version>
<opentelemetry-alpha.version>1.28.0-alpha</opentelemetry-alpha.version>
</properties>
<scm>
<connection>scm:git:[email protected]:quarkiverse/quarkus-google-cloud-services.git</connection>
Expand Down Expand Up @@ -63,6 +64,11 @@
<artifactId>assertj-core</artifactId>
<version>${assertj.version}</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-opencensus-shim</artifactId>
<version>${opentelemetry-alpha.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
Expand Down

0 comments on commit 8ec386e

Please sign in to comment.