From 7480d43b1fb0692c0f7f5e10ac3516b6e3b9fcb3 Mon Sep 17 00:00:00 2001 From: Pranav Sharma Date: Fri, 10 Jan 2025 15:05:10 -0500 Subject: [PATCH] Add GCP authentication extension (#1631) --- .github/component_owners.yml | 3 + .github/scripts/draft-change-log-entries.sh | 1 + README.md | 1 + gcp-auth-extension/README.md | 124 +++++++++ gcp-auth-extension/build.gradle.kts | 114 ++++++++ gcp-auth-extension/gradle.properties | 2 + .../contrib/gcp/auth/ConfigurableOption.java | 93 +++++++ ...thAutoConfigurationCustomizerProvider.java | 126 +++++++++ .../contrib/gcp/auth/GoogleAuthException.java | 54 ++++ ...toConfigurationCustomizerProviderTest.java | 245 ++++++++++++++++++ .../auth/GcpAuthExtensionEndToEndTest.java | 213 +++++++++++++++ .../gcp/auth/springapp/Application.java | 17 ++ .../gcp/auth/springapp/Controller.java | 38 +++ .../src/test/resources/fakecreds.json | 13 + .../org.mockito.plugins.MockMaker | 1 + settings.gradle.kts | 1 + 16 files changed, 1046 insertions(+) create mode 100644 gcp-auth-extension/README.md create mode 100644 gcp-auth-extension/build.gradle.kts create mode 100644 gcp-auth-extension/gradle.properties create mode 100644 gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/ConfigurableOption.java create mode 100644 gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/GcpAuthAutoConfigurationCustomizerProvider.java create mode 100644 gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/GoogleAuthException.java create mode 100644 gcp-auth-extension/src/test/java/io/opentelemetry/contrib/gcp/auth/GcpAuthAutoConfigurationCustomizerProviderTest.java create mode 100644 gcp-auth-extension/src/test/java/io/opentelemetry/contrib/gcp/auth/GcpAuthExtensionEndToEndTest.java create mode 100644 gcp-auth-extension/src/test/java/io/opentelemetry/contrib/gcp/auth/springapp/Application.java create mode 100644 gcp-auth-extension/src/test/java/io/opentelemetry/contrib/gcp/auth/springapp/Controller.java create mode 100644 gcp-auth-extension/src/test/resources/fakecreds.json create mode 100644 gcp-auth-extension/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/.github/component_owners.yml b/.github/component_owners.yml index b8dc57856..f5211ac91 100644 --- a/.github/component_owners.yml +++ b/.github/component_owners.yml @@ -33,6 +33,9 @@ components: gcp-resources: - jsuereth - psx95 + gcp-auth-extension: + - jsuereth + - psx95 jfr-connection: - breedx-splk - jeanbisutti diff --git a/.github/scripts/draft-change-log-entries.sh b/.github/scripts/draft-change-log-entries.sh index 345440cb2..8d0a011cb 100755 --- a/.github/scripts/draft-change-log-entries.sh +++ b/.github/scripts/draft-change-log-entries.sh @@ -32,6 +32,7 @@ component_names["compressors/"]="Compressors" component_names["consistent-sampling/"]="Consistent sampling" component_names["disk-buffering/"]="Disk buffering" component_names["gcp-resources/"]="GCP Resources" +component_names["gcp-auth-extension/"]="GCP authentication extension" component_names["inferred-spans/"]="Inferred spans" component_names["jfr-connection/"]="JFR connection" component_names["jfr-events/"]="JFR events" diff --git a/README.md b/README.md index 6502fea65..dd5408ba6 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ feature or via instrumentation, this project is hopefully for you. | alpha | [zstd Compressor](./compressors/compressor-zstd/README.md) | | alpha | [Consistent Sampling](./consistent-sampling/README.md) | | alpha | [Disk Buffering](./disk-buffering/README.md) | +| alpha | [GCP Authentication Extension](./gcp-auth-extension/README.md) | | beta | [GCP Resources](./gcp-resources/README.md) | | beta | [Inferred Spans](./inferred-spans/README.md) | | alpha | [JFR Connection](./jfr-connection/README.md) | diff --git a/gcp-auth-extension/README.md b/gcp-auth-extension/README.md new file mode 100644 index 000000000..36d6504eb --- /dev/null +++ b/gcp-auth-extension/README.md @@ -0,0 +1,124 @@ +# Google Cloud Authentication Extension + +The Google Cloud Auth Extension allows the users to export telemetry from their applications to Google Cloud using the built-in OTLP exporters.\ +The extension takes care of the necessary configuration required to authenticate to GCP to successfully export telemetry. + +## Prerequisites + +### Ensure the presence of Google Cloud Credentials on your machine/environment + +```shell +gcloud auth application-default login +``` + +Executing this command will save your application credentials to default path which will depend on the type of machine - + +- Linux, macOS: `$HOME/.config/gcloud/application_default_credentials.json` +- Windows: `%APPDATA%\gcloud\application_default_credentials.json` + +**NOTE: This method of authentication is not recommended for production environments.** + +Next, export the credentials to `GOOGLE_APPLICATION_CREDENTIALS` environment variable - + +For Linux & MacOS: + +```shell +export GOOGLE_APPLICATION_CREDENTIALS=$HOME/.config/gcloud/application_default_credentials.json +``` + +These credentials are built-in running in a Google App Engine, Google Cloud Shell or Google Compute Engine environment. + +### Configuring the extension + +The extension can be configured either by environment variables or system properties. + +Here is a list of configurable options for the extension: + +- `GOOGLE_CLOUD_PROJECT`: Environment variable that represents the Google Cloud Project ID to which the telemetry needs to be exported. + - Can also be configured using `google.cloud.project` system property. + - If this option is not configured, the extension would infer GCP Project ID from the application default credentials. For more information on application default credentials, see [here](https://cloud.google.com/docs/authentication/application-default-credentials). + +## Usage + +### With OpenTelemetry Java agent + +The OpenTelemetry Java Agent Extension can be easily added to any Java application by modifying the startup command to the application. +For more information on Extensions, see the [documentation here](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/examples/extension/README.md). + +Below is a snippet showing how to add the extension to a Java application using the Gradle build system. + +```gradle +// Specify OpenTelemetry Autoinstrumentation Java Agent Path. +def otelAgentPath = +// Specify the path for Google Cloud Authentication Extension for the Java Agent. +def extensionPath = +def googleCloudProjectId = +def googleOtlpEndpoint = + +val autoconf_config = listOf( + "-javaagent:${otelAgentPath}", + "-Dotel.javaagent.extensions=${extensionPath}", + // Configure the GCP Auth extension using system properties. + // This can also be configured using environment variables. + "-Dgoogle.cloud.project=${googleCloudProjectId}", + // Configure auto instrumentation. + "-Dotel.exporter.otlp.traces.endpoint=${googleOtlpEndpoint}", + '-Dotel.java.global-autoconfigure.enabled=true', + // Optionally enable the built-in GCP resource detector + '-Dotel.resource.providers.gcp.enabled=true' + '-Dotel.traces.exporter=otlp', + '-Dotel.metrics.exporter=logging' +) + +application { + ... + applicationDefaultJvmArgs = autoconf_config + ... +} +``` + +### Without OpenTelemetry Java agent + +This extension can be used without the OpenTelemetry Java agent by leveraging the [OpenTelemetry SDK Autoconfigure](https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/autoconfigure/README.md) module.\ +When using the autoconfigured SDK, simply adding this extension as a dependency automatically configures authentication headers and resource attributes for spans, enabling export to Google Cloud. + +Below is a snippet showing how to use this extension as a dependency when the application is not instrumented using the OpenTelemetry Java agent. + +```gradle +dependencies { + implementation("io.opentelemetry:opentelemetry-api") + implementation("io.opentelemetry:opentelemetry-sdk") + implementation("io.opentelemetry:opentelemetry-exporter-otlp") + implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") + // include the auth extension dependency + implementation("io.opentelemetry.contrib:opentelemetry-gcp-auth-extension") + + // other dependencies + ... + +} + +val autoconf_config = listOf( + '-Dgoogle.cloud.project=your-gcp-project-id', + '-Dotel.exporter.otlp.endpoint=https://your.otlp.endpoint:1234', + '-Dotel.traces.exporter=otlp', + '-Dotel.java.global-autoconfigure.enabled=true' + + // any additional args + ... +) + +application { + applicationDefaultJvmArgs = autoconf_config + + // additional configuration + ... +} +``` + +## Component Owners + +- [Josh Suereth](https://github.com/jsuereth), Google +- [Pranav Sharma](https://github.com/psx95), Google + +Learn more about component owners in [component_owners.yml](../.github/component_owners.yml). diff --git a/gcp-auth-extension/build.gradle.kts b/gcp-auth-extension/build.gradle.kts new file mode 100644 index 000000000..8da56a85e --- /dev/null +++ b/gcp-auth-extension/build.gradle.kts @@ -0,0 +1,114 @@ +plugins { + id("otel.java-conventions") + id("otel.publish-conventions") + id("com.github.johnrengelman.shadow") + id("org.springframework.boot") version "2.7.18" +} + +description = "OpenTelemetry extension that provides GCP authentication support for OTLP exporters" +otelJava.moduleName.set("io.opentelemetry.contrib.gcp.auth") + +val agent: Configuration by configurations.creating { + isCanBeResolved = true + isCanBeConsumed = false +} + +dependencies { + annotationProcessor("com.google.auto.service:auto-service") + // We use `compileOnly` dependency because during runtime all necessary classes are provided by + // javaagent itself. + compileOnly("com.google.auto.service:auto-service-annotations") + compileOnly("io.opentelemetry:opentelemetry-api") + compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") + compileOnly("io.opentelemetry:opentelemetry-exporter-otlp") + + // Only dependencies added to `implementation` configuration will be picked up by Shadow plugin + implementation("com.google.auth:google-auth-library-oauth2-http:1.30.1") + + // Test dependencies + testCompileOnly("com.google.auto.service:auto-service-annotations") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") + testImplementation("org.junit.jupiter:junit-jupiter-api") + + testImplementation("io.opentelemetry:opentelemetry-api") + testImplementation("io.opentelemetry:opentelemetry-exporter-otlp") + testImplementation("io.opentelemetry:opentelemetry-sdk-testing") + testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") + testImplementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations") + + testImplementation("org.awaitility:awaitility") + testImplementation("org.mockito:mockito-inline") + testImplementation("org.mockito:mockito-junit-jupiter") + testImplementation("org.mock-server:mockserver-netty:5.15.0") + testImplementation("io.opentelemetry.proto:opentelemetry-proto:1.4.0-alpha") + testImplementation("org.springframework.boot:spring-boot-starter-web:2.7.18") + testImplementation("org.springframework.boot:spring-boot-starter:2.7.18") + testImplementation("org.springframework.boot:spring-boot-starter-test:2.7.18") + + agent("io.opentelemetry.javaagent:opentelemetry-javaagent") +} + +tasks { + test { + useJUnitPlatform() + // exclude integration test + exclude("io/opentelemetry/contrib/gcp/auth/GcpAuthExtensionEndToEndTest.class") + } + + shadowJar { + archiveClassifier.set("") + } + + jar { + // Disable standard jar + enabled = false + } + + assemble { + dependsOn(shadowJar) + } + + bootJar { + // disable bootJar in build since it only runs as part of test + enabled = false + } +} + +val builtLibsDir = layout.buildDirectory.dir("libs").get().asFile.absolutePath +val javaAgentJarPath = "$builtLibsDir/otel-agent.jar" +val authExtensionJarPath = "${tasks.shadowJar.get().archiveFile.get()}" + +tasks.register("copyAgent") { + into(layout.buildDirectory.dir("libs")) + from(configurations.named("agent") { + rename("opentelemetry-javaagent(.*).jar", "otel-agent.jar") + }) +} + +tasks.register("IntegrationTest") { + dependsOn(tasks.shadowJar) + dependsOn(tasks.named("copyAgent")) + + useJUnitPlatform() + // include only the integration test file + include("io/opentelemetry/contrib/gcp/auth/GcpAuthExtensionEndToEndTest.class") + + val fakeCredsFilePath = project.file("src/test/resources/fakecreds.json").absolutePath + + environment("GOOGLE_CLOUD_QUOTA_PROJECT", "quota-project-id") + environment("GOOGLE_APPLICATION_CREDENTIALS", fakeCredsFilePath) + jvmArgs = listOf( + "-javaagent:$javaAgentJarPath", + "-Dotel.javaagent.extensions=$authExtensionJarPath", + "-Dgoogle.cloud.project=my-gcp-project", + "-Dotel.java.global-autoconfigure.enabled=true", + "-Dotel.exporter.otlp.endpoint=http://localhost:4318", + "-Dotel.resource.providers.gcp.enabled=true", + "-Dotel.traces.exporter=otlp", + "-Dotel.bsp.schedule.delay=2000", + "-Dotel.metrics.exporter=none", + "-Dotel.logs.exporter=none", + "-Dotel.exporter.otlp.protocol=http/protobuf", + "-Dmockserver.logLevel=off" + ) +} diff --git a/gcp-auth-extension/gradle.properties b/gcp-auth-extension/gradle.properties new file mode 100644 index 000000000..a0402e1e2 --- /dev/null +++ b/gcp-auth-extension/gradle.properties @@ -0,0 +1,2 @@ +# TODO: uncomment when ready to mark as stable +# otel.stable=true diff --git a/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/ConfigurableOption.java b/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/ConfigurableOption.java new file mode 100644 index 000000000..520c59db2 --- /dev/null +++ b/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/ConfigurableOption.java @@ -0,0 +1,93 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.gcp.auth; + +import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException; +import java.util.Locale; +import java.util.function.Supplier; + +/** + * An enum representing configurable options for a GCP Authentication Extension. Each option has a + * user-readable name and can be configured using environment variables or system properties. + */ +public enum ConfigurableOption { + /** + * Represents the Google Cloud Project ID option. Can be configured using the environment variable + * `GOOGLE_CLOUD_PROJECT` or the system property `google.cloud.project`. + */ + GOOGLE_CLOUD_PROJECT("Google Cloud Project ID"); + + private final String userReadableName; + private final String environmentVariableName; + private final String systemPropertyName; + + ConfigurableOption(String userReadableName) { + this.userReadableName = userReadableName; + this.environmentVariableName = this.name(); + this.systemPropertyName = + this.environmentVariableName.toLowerCase(Locale.ENGLISH).replace('_', '.'); + } + + /** + * Returns the environment variable name associated with this option. + * + * @return the environment variable name (e.g., GOOGLE_CLOUD_PROJECT) + */ + String getEnvironmentVariable() { + return this.environmentVariableName; + } + + /** + * Returns the system property name associated with this option. + * + * @return the system property name (e.g., google.cloud.project) + */ + String getSystemProperty() { + return this.systemPropertyName; + } + + /** + * Retrieves the configured value for this option. This method checks the environment variable + * first and then the system property. + * + * @return The configured value as a string, or throws an exception if not configured. + * @throws ConfigurationException if neither the environment variable nor the system property is + * set. + */ + String getConfiguredValue() { + String envVar = System.getenv(this.getEnvironmentVariable()); + String sysProp = System.getProperty(this.getSystemProperty()); + + if (envVar != null && !envVar.isEmpty()) { + return envVar; + } else if (sysProp != null && !sysProp.isEmpty()) { + return sysProp; + } else { + throw new ConfigurationException( + String.format( + "GCP Authentication Extension not configured properly: %s not configured. Configure it by exporting environment variable %s or system property %s", + this.userReadableName, this.getEnvironmentVariable(), this.getSystemProperty())); + } + } + + /** + * Retrieves the value for this option, prioritizing environment variables and system properties. + * If neither an environment variable nor a system property is set for this option, the provided + * fallback function is used to determine the value. + * + * @param fallback A {@link Supplier} that provides the default value for the option when it is + * not explicitly configured via an environment variable or system property. + * @return The configured value for the option, obtained from the environment variable, system + * property, or the fallback function, in that order of precedence. + */ + String getConfiguredValueWithFallback(Supplier fallback) { + try { + return this.getConfiguredValue(); + } catch (ConfigurationException e) { + return fallback.get(); + } + } +} diff --git a/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/GcpAuthAutoConfigurationCustomizerProvider.java b/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/GcpAuthAutoConfigurationCustomizerProvider.java new file mode 100644 index 000000000..2fa1a666d --- /dev/null +++ b/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/GcpAuthAutoConfigurationCustomizerProvider.java @@ -0,0 +1,126 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.gcp.auth; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.auto.service.AutoService; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.contrib.gcp.auth.GoogleAuthException.Reason; +import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; +import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporterBuilder; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporterBuilder; +import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer; +import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * An AutoConfigurationCustomizerProvider for Google Cloud Platform (GCP) OpenTelemetry (OTLP) + * integration. + * + *

This class is registered as a service provider using {@link AutoService} and is responsible + * for customizing the OpenTelemetry configuration for GCP specific behavior. It retrieves Google + * Application Default Credentials (ADC) and adds them as authorization headers to the configured + * {@link SpanExporter}. It also sets default properties and resource attributes for GCP + * integration. + * + * @see AutoConfigurationCustomizerProvider + * @see GoogleCredentials + */ +@AutoService(AutoConfigurationCustomizerProvider.class) +public class GcpAuthAutoConfigurationCustomizerProvider + implements AutoConfigurationCustomizerProvider { + + static final String QUOTA_USER_PROJECT_HEADER = "X-Goog-User-Project"; + static final String GCP_USER_PROJECT_ID_KEY = "gcp.project_id"; + + /** + * Customizes the provided {@link AutoConfigurationCustomizer}. + * + *

This method attempts to retrieve Google Application Default Credentials (ADC) and performs + * the following: - Adds authorization headers to the configured {@link SpanExporter} based on the + * retrieved credentials. - Adds default properties for OTLP endpoint and resource attributes for + * GCP integration. + * + * @param autoConfiguration the AutoConfigurationCustomizer to customize. + * @throws GoogleAuthException if there's an error retrieving Google Application Default + * Credentials. + * @throws io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException if required options are + * not configured through environment variables or system properties. + */ + @Override + public void customize(AutoConfigurationCustomizer autoConfiguration) { + try { + GoogleCredentials credentials = GoogleCredentials.getApplicationDefault(); + autoConfiguration + .addSpanExporterCustomizer( + (exporter, configProperties) -> addAuthorizationHeaders(exporter, credentials)) + .addResourceCustomizer(GcpAuthAutoConfigurationCustomizerProvider::customizeResource); + } catch (IOException e) { + throw new GoogleAuthException(Reason.FAILED_ADC_RETRIEVAL, e); + } + } + + @Override + public int order() { + return Integer.MAX_VALUE - 1; + } + + // Adds authorization headers to the calls made by the OtlpGrpcSpanExporter and + // OtlpHttpSpanExporter. + private static SpanExporter addAuthorizationHeaders( + SpanExporter exporter, GoogleCredentials credentials) { + if (exporter instanceof OtlpHttpSpanExporter) { + OtlpHttpSpanExporterBuilder builder = + ((OtlpHttpSpanExporter) exporter) + .toBuilder().setHeaders(() -> getRequiredHeaderMap(credentials)); + return builder.build(); + } else if (exporter instanceof OtlpGrpcSpanExporter) { + OtlpGrpcSpanExporterBuilder builder = + ((OtlpGrpcSpanExporter) exporter) + .toBuilder().setHeaders(() -> getRequiredHeaderMap(credentials)); + return builder.build(); + } + return exporter; + } + + private static Map getRequiredHeaderMap(GoogleCredentials credentials) { + Map gcpHeaders = new HashMap<>(); + try { + credentials.refreshIfExpired(); + } catch (IOException e) { + throw new GoogleAuthException(Reason.FAILED_ADC_REFRESH, e); + } + gcpHeaders.put(QUOTA_USER_PROJECT_HEADER, credentials.getQuotaProjectId()); + gcpHeaders.put("Authorization", "Bearer " + credentials.getAccessToken().getTokenValue()); + return gcpHeaders; + } + + // Updates the current resource with the attributes required for ingesting OTLP data on GCP. + private static Resource customizeResource(Resource resource, ConfigProperties configProperties) { + String gcpProjectId = + ConfigurableOption.GOOGLE_CLOUD_PROJECT.getConfiguredValueWithFallback( + () -> { + try { + GoogleCredentials googleCredentials = GoogleCredentials.getApplicationDefault(); + return googleCredentials.getQuotaProjectId(); + } catch (IOException e) { + throw new GoogleAuthException(Reason.FAILED_ADC_RETRIEVAL, e); + } + }); + + Resource res = + Resource.create( + Attributes.of(AttributeKey.stringKey(GCP_USER_PROJECT_ID_KEY), gcpProjectId)); + return resource.merge(res); + } +} diff --git a/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/GoogleAuthException.java b/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/GoogleAuthException.java new file mode 100644 index 000000000..2f6335f52 --- /dev/null +++ b/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/GoogleAuthException.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.gcp.auth; + +/** + * An unchecked exception indicating a failure during Google authentication. This exception is + * thrown when there are issues with retrieving or refreshing Google Application Default Credentials + * (ADC). + */ +public class GoogleAuthException extends RuntimeException { + + private static final long serialVersionUID = 149908685226796448L; + + /** + * Constructs a new {@code GoogleAuthException} with the specified reason and cause. + * + * @param reason the reason for the authentication failure. + * @param cause the underlying cause of the exception (e.g., an IOException). + */ + GoogleAuthException(Reason reason, Throwable cause) { + super(reason.message, cause); + } + + /** Enumerates the possible reasons for a Google authentication failure. */ + enum Reason { + /** Indicates a failure to retrieve Google Application Default Credentials. */ + FAILED_ADC_RETRIEVAL("Unable to retrieve Google Application Default Credentials."), + /** Indicates a failure to retrieve Google Application Default Credentials. */ + FAILED_ADC_REFRESH("Unable to refresh Google Application Default Credentials."); + + private final String message; + + /** + * Constructs a new {@code Reason} with the specified message. + * + * @param message the message describing the reason. + */ + Reason(String message) { + this.message = message; + } + + /** + * Returns the message associated with this reason. + * + * @return the message describing the reason. + */ + public String getMessage() { + return message; + } + } +} diff --git a/gcp-auth-extension/src/test/java/io/opentelemetry/contrib/gcp/auth/GcpAuthAutoConfigurationCustomizerProviderTest.java b/gcp-auth-extension/src/test/java/io/opentelemetry/contrib/gcp/auth/GcpAuthAutoConfigurationCustomizerProviderTest.java new file mode 100644 index 000000000..c03d14c92 --- /dev/null +++ b/gcp-auth-extension/src/test/java/io/opentelemetry/contrib/gcp/auth/GcpAuthAutoConfigurationCustomizerProviderTest.java @@ -0,0 +1,245 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.gcp.auth; + +import static io.opentelemetry.contrib.gcp.auth.GcpAuthAutoConfigurationCustomizerProvider.GCP_USER_PROJECT_ID_KEY; +import static io.opentelemetry.contrib.gcp.auth.GcpAuthAutoConfigurationCustomizerProvider.QUOTA_USER_PROJECT_HEADER; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Scope; +import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; +import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporterBuilder; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporterBuilder; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdkBuilder; +import io.opentelemetry.sdk.autoconfigure.internal.AutoConfigureUtil; +import io.opentelemetry.sdk.autoconfigure.internal.ComponentLoader; +import io.opentelemetry.sdk.autoconfigure.internal.SpiHelper; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.time.Duration; +import java.time.Instant; +import java.util.AbstractMap.SimpleEntry; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class GcpAuthAutoConfigurationCustomizerProviderTest { + + @Mock private GoogleCredentials mockedGoogleCredentials; + + @Captor private ArgumentCaptor>> headerSupplierCaptor; + + private static final ImmutableMap otelProperties = + ImmutableMap.of( + "otel.traces.exporter", + "otlp", + "otel.metrics.exporter", + "none", + "otel.logs.exporter", + "none", + "otel.resource.attributes", + "foo=bar"); + + @BeforeEach + @SuppressWarnings("CannotMockMethod") + public void setup() { + MockitoAnnotations.openMocks(this); + Mockito.when(mockedGoogleCredentials.getQuotaProjectId()).thenReturn("test-project"); + Mockito.when(mockedGoogleCredentials.getAccessToken()) + .thenReturn(new AccessToken("fake", Date.from(Instant.now()))); + } + + @Test + public void testCustomizerOtlpHttp() { + OtlpHttpSpanExporter mockOtlpHttpSpanExporter = Mockito.mock(OtlpHttpSpanExporter.class); + OtlpHttpSpanExporterBuilder otlpSpanExporterBuilder = OtlpHttpSpanExporter.builder(); + OtlpHttpSpanExporterBuilder spyOtlpHttpSpanExporterBuilder = + Mockito.spy(otlpSpanExporterBuilder); + Mockito.when(spyOtlpHttpSpanExporterBuilder.build()).thenReturn(mockOtlpHttpSpanExporter); + + Mockito.when(mockOtlpHttpSpanExporter.shutdown()).thenReturn(CompletableResultCode.ofSuccess()); + List exportedSpans = new ArrayList<>(); + Mockito.when(mockOtlpHttpSpanExporter.export(Mockito.anyCollection())) + .thenAnswer( + invocationOnMock -> { + exportedSpans.addAll(invocationOnMock.getArgument(0)); + return CompletableResultCode.ofSuccess(); + }); + Mockito.when(mockOtlpHttpSpanExporter.toBuilder()).thenReturn(spyOtlpHttpSpanExporterBuilder); + + try (MockedStatic googleCredentialsMockedStatic = + Mockito.mockStatic(GoogleCredentials.class)) { + googleCredentialsMockedStatic + .when(GoogleCredentials::getApplicationDefault) + .thenReturn(mockedGoogleCredentials); + + OpenTelemetrySdk sdk = buildOpenTelemetrySdkWithExporter(mockOtlpHttpSpanExporter); + generateTestSpan(sdk); + CompletableResultCode code = sdk.shutdown(); + CompletableResultCode joinResult = code.join(10, TimeUnit.SECONDS); + assertTrue(joinResult.isSuccess()); + + Mockito.verify(mockOtlpHttpSpanExporter, Mockito.times(1)).toBuilder(); + Mockito.verify(spyOtlpHttpSpanExporterBuilder, Mockito.times(1)) + .setHeaders(headerSupplierCaptor.capture()); + assertEquals(2, headerSupplierCaptor.getValue().get().size()); + assertThat(verifyAuthHeaders(headerSupplierCaptor.getValue().get())).isTrue(); + + Mockito.verify(mockOtlpHttpSpanExporter, Mockito.atLeast(1)).export(Mockito.anyCollection()); + + assertThat(exportedSpans) + .hasSizeGreaterThan(0) + .allSatisfy( + spanData -> { + assertThat(spanData.getResource().getAttributes().asMap()) + .containsEntry(AttributeKey.stringKey(GCP_USER_PROJECT_ID_KEY), "test-project") + .containsEntry(AttributeKey.stringKey("foo"), "bar"); + assertThat(spanData.getAttributes().asMap()) + .containsKey(AttributeKey.longKey("work_loop")); + }); + } + } + + @Test + public void testCustomizerOtlpGrpc() { + OtlpGrpcSpanExporter mockOtlpGrpcSpanExporter = Mockito.mock(OtlpGrpcSpanExporter.class); + OtlpGrpcSpanExporterBuilder otlpSpanExporterBuilder = OtlpGrpcSpanExporter.builder(); + OtlpGrpcSpanExporterBuilder spyOtlpGrpcSpanExporterBuilder = + Mockito.spy(otlpSpanExporterBuilder); + Mockito.when(spyOtlpGrpcSpanExporterBuilder.build()).thenReturn(mockOtlpGrpcSpanExporter); + Mockito.when(mockOtlpGrpcSpanExporter.shutdown()).thenReturn(CompletableResultCode.ofSuccess()); + List exportedSpans = new ArrayList<>(); + Mockito.when(mockOtlpGrpcSpanExporter.export(Mockito.anyCollection())) + .thenAnswer( + invocationOnMock -> { + exportedSpans.addAll(invocationOnMock.getArgument(0)); + return CompletableResultCode.ofSuccess(); + }); + Mockito.when(mockOtlpGrpcSpanExporter.toBuilder()).thenReturn(spyOtlpGrpcSpanExporterBuilder); + + try (MockedStatic googleCredentialsMockedStatic = + Mockito.mockStatic(GoogleCredentials.class)) { + googleCredentialsMockedStatic + .when(GoogleCredentials::getApplicationDefault) + .thenReturn(mockedGoogleCredentials); + + OpenTelemetrySdk sdk = buildOpenTelemetrySdkWithExporter(mockOtlpGrpcSpanExporter); + generateTestSpan(sdk); + CompletableResultCode code = sdk.shutdown(); + CompletableResultCode joinResult = code.join(10, TimeUnit.SECONDS); + assertTrue(joinResult.isSuccess()); + + Mockito.verify(mockOtlpGrpcSpanExporter, Mockito.times(1)).toBuilder(); + Mockito.verify(spyOtlpGrpcSpanExporterBuilder, Mockito.times(1)) + .setHeaders(headerSupplierCaptor.capture()); + assertEquals(2, headerSupplierCaptor.getValue().get().size()); + verifyAuthHeaders(headerSupplierCaptor.getValue().get()); + + Mockito.verify(mockOtlpGrpcSpanExporter, Mockito.atLeast(1)).export(Mockito.anyCollection()); + + assertThat(exportedSpans) + .hasSizeGreaterThan(0) + .allSatisfy( + spanData -> { + assertThat(spanData.getResource().getAttributes().asMap()) + .containsEntry(AttributeKey.stringKey(GCP_USER_PROJECT_ID_KEY), "test-project") + .containsEntry(AttributeKey.stringKey("foo"), "bar"); + assertThat(spanData.getAttributes().asMap()) + .containsKey(AttributeKey.longKey("work_loop")); + }); + } + } + + private OpenTelemetrySdk buildOpenTelemetrySdkWithExporter(SpanExporter spanExporter) { + SpiHelper spiHelper = + SpiHelper.create(GcpAuthAutoConfigurationCustomizerProviderTest.class.getClassLoader()); + AutoConfiguredOpenTelemetrySdkBuilder builder = + AutoConfiguredOpenTelemetrySdk.builder().addPropertiesSupplier(() -> otelProperties); + AutoConfigureUtil.setComponentLoader( + builder, + new ComponentLoader() { + @SuppressWarnings("unchecked") + @Override + public List load(Class spiClass) { + if (spiClass == ConfigurableSpanExporterProvider.class) { + return Collections.singletonList( + (T) + new ConfigurableSpanExporterProvider() { + @Override + public SpanExporter createExporter(ConfigProperties configProperties) { + return spanExporter; + } + + @Override + public String getName() { + return "otlp"; + } + }); + } + return spiHelper.load(spiClass); + } + }); + return builder.build().getOpenTelemetrySdk(); + } + + private static boolean verifyAuthHeaders(Map headers) { + Set> headerEntrySet = headers.entrySet(); + return headerEntrySet.contains(new SimpleEntry<>(QUOTA_USER_PROJECT_HEADER, "test-project")) + && headerEntrySet.contains(new SimpleEntry<>("Authorization", "Bearer fake")); + } + + private static void generateTestSpan(OpenTelemetrySdk openTelemetrySdk) { + Span span = openTelemetrySdk.getTracer("test").spanBuilder("sample").startSpan(); + try (Scope ignored = span.makeCurrent()) { + long workOutput = busyloop(); + span.setAttribute("work_loop", workOutput); + } finally { + span.end(); + } + } + + // loop to simulate work done + private static long busyloop() { + Instant start = Instant.now(); + Instant end; + long counter = 0; + do { + counter++; + end = Instant.now(); + } while (Duration.between(start, end).toMillis() < 1000); + return counter; + } +} diff --git a/gcp-auth-extension/src/test/java/io/opentelemetry/contrib/gcp/auth/GcpAuthExtensionEndToEndTest.java b/gcp-auth-extension/src/test/java/io/opentelemetry/contrib/gcp/auth/GcpAuthExtensionEndToEndTest.java new file mode 100644 index 000000000..421d4fcec --- /dev/null +++ b/gcp-auth-extension/src/test/java/io/opentelemetry/contrib/gcp/auth/GcpAuthExtensionEndToEndTest.java @@ -0,0 +1,213 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.gcp.auth; + +import static io.opentelemetry.contrib.gcp.auth.GcpAuthAutoConfigurationCustomizerProvider.GCP_USER_PROJECT_ID_KEY; +import static io.opentelemetry.contrib.gcp.auth.GcpAuthAutoConfigurationCustomizerProvider.QUOTA_USER_PROJECT_HEADER; +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockserver.model.HttpRequest.request; +import static org.mockserver.model.HttpResponse.response; +import static org.mockserver.stop.Stop.stopQuietly; + +import com.google.protobuf.InvalidProtocolBufferException; +import io.opentelemetry.contrib.gcp.auth.springapp.Application; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; +import io.opentelemetry.proto.common.v1.AnyValue; +import io.opentelemetry.proto.common.v1.KeyValue; +import io.opentelemetry.proto.trace.v1.ResourceSpans; +import java.net.URI; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockserver.client.MockServerClient; +import org.mockserver.integration.ClientAndServer; +import org.mockserver.model.Body; +import org.mockserver.model.Headers; +import org.mockserver.model.HttpRequest; +import org.mockserver.model.JsonBody; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.server.LocalServerPort; + +@SpringBootTest( + classes = {Application.class}, + webEnvironment = WebEnvironment.RANDOM_PORT) +public class GcpAuthExtensionEndToEndTest { + + @LocalServerPort private int testApplicationPort; // port at which the spring app is running + + @Autowired private TestRestTemplate template; + + // The port at which the backend server will receive telemetry + private static final int EXPORTER_ENDPOINT_PORT = 4318; + // The port at which the mock GCP OAuth 2.0 server will run + private static final int MOCK_GCP_OAUTH2_PORT = 8090; + + // Backend server to which the application under test will export traces + // the export config is specified in the build.gradle file. + private static ClientAndServer backendServer; + + // Mock server to intercept calls to the GCP OAuth 2.0 server and provide fake credentials + private static ClientAndServer mockGcpOAuth2Server; + + private static final String DUMMY_GCP_QUOTA_PROJECT = System.getenv("GOOGLE_CLOUD_QUOTA_PROJECT"); + private static final String DUMMY_GCP_PROJECT = System.getProperty("google.cloud.project"); + + @BeforeAll + public static void setup() throws NoSuchAlgorithmException, KeyManagementException { + // Setup proxy host(s) + System.setProperty("http.proxyHost", "localhost"); + System.setProperty("http.proxyPort", MOCK_GCP_OAUTH2_PORT + ""); + System.setProperty("https.proxyHost", "localhost"); + System.setProperty("https.proxyPort", MOCK_GCP_OAUTH2_PORT + ""); + System.setProperty("http.nonProxyHost", "localhost"); + System.setProperty("https.nonProxyHost", "localhost"); + + // Disable SSL validation for integration test + // The OAuth2 token validation requires SSL validation + disableSSLValidation(); + + // Set up mock OTLP backend server to which traces will be exported + backendServer = ClientAndServer.startClientAndServer(EXPORTER_ENDPOINT_PORT); + backendServer.when(request()).respond(response().withStatusCode(200)); + + // Set up the mock gcp metadata server to provide fake credentials + String accessTokenResponse = + "{\"access_token\": \"fake.access_token\",\"expires_in\": 3600, \"token_type\": \"Bearer\"}"; + mockGcpOAuth2Server = ClientAndServer.startClientAndServer(MOCK_GCP_OAUTH2_PORT); + + MockServerClient mockServerClient = + new MockServerClient("localhost", MOCK_GCP_OAUTH2_PORT).withSecure(true); + + // mock the token refresh - always respond with 200 + mockServerClient + .when(request().withMethod("POST").withPath("/token")) + .respond( + response() + .withStatusCode(200) + .withHeader("Content-Type", "application/json") + .withBody(new JsonBody(accessTokenResponse))); + } + + @AfterAll + public static void teardown() { + // Stop the backend server + stopQuietly(backendServer); + stopQuietly(mockGcpOAuth2Server); + } + + @Test + public void authExtensionSmokeTest() { + template.getForEntity( + URI.create("http://localhost:" + testApplicationPort + "/ping"), String.class); + + await() + .atMost(Duration.ofSeconds(10)) + .untilAsserted( + () -> { + HttpRequest[] requests = backendServer.retrieveRecordedRequests(request()); + List extractedHeaders = extractHeadersFromRequests(requests); + verifyRequestHeaders(extractedHeaders); + + List extractedResourceSpans = + extractResourceSpansFromRequests(requests); + verifyResourceAttributes(extractedResourceSpans); + }); + } + + // Helper methods + + private static void disableSSLValidation() + throws NoSuchAlgorithmException, KeyManagementException { + TrustManager[] trustAllCerts = + new TrustManager[] { + new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) {} + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) {} + + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } + } + }; + SSLContext sc = SSLContext.getInstance("SSL"); + sc.init(null, trustAllCerts, new java.security.SecureRandom()); + HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); + } + + private static void verifyResourceAttributes(List extractedResourceSpans) { + extractedResourceSpans.forEach( + resourceSpan -> + assertTrue( + resourceSpan + .getResource() + .getAttributesList() + .contains( + KeyValue.newBuilder() + .setKey(GCP_USER_PROJECT_ID_KEY) + .setValue(AnyValue.newBuilder().setStringValue(DUMMY_GCP_PROJECT)) + .build()))); + } + + private static void verifyRequestHeaders(List extractedHeaders) { + assertFalse(extractedHeaders.isEmpty()); + // verify if extension added the required headers + extractedHeaders.forEach( + headers -> { + assertTrue(headers.containsEntry(QUOTA_USER_PROJECT_HEADER, DUMMY_GCP_QUOTA_PROJECT)); + assertTrue(headers.containsEntry("Authorization", "Bearer fake.access_token")); + }); + } + + private static List extractHeadersFromRequests(HttpRequest[] requests) { + return Arrays.stream(requests).map(HttpRequest::getHeaders).collect(Collectors.toList()); + } + + /** + * Extract resource spans from http requests received by a telemetry collector. + * + * @param requests Request received by a http server trace collector + * @return spans extracted from the request body + */ + private static List extractResourceSpansFromRequests(HttpRequest[] requests) { + return Arrays.stream(requests) + .map(HttpRequest::getBody) + .map(GcpAuthExtensionEndToEndTest::getExportTraceServiceRequest) + .filter(Optional::isPresent) + .map(Optional::get) + .flatMap( + exportTraceServiceRequest -> exportTraceServiceRequest.getResourceSpansList().stream()) + .collect(Collectors.toList()); + } + + private static Optional getExportTraceServiceRequest(Body body) { + try { + return Optional.ofNullable(ExportTraceServiceRequest.parseFrom(body.getRawBytes())); + } catch (InvalidProtocolBufferException e) { + return Optional.empty(); + } + } +} diff --git a/gcp-auth-extension/src/test/java/io/opentelemetry/contrib/gcp/auth/springapp/Application.java b/gcp-auth-extension/src/test/java/io/opentelemetry/contrib/gcp/auth/springapp/Application.java new file mode 100644 index 000000000..74ceff1ca --- /dev/null +++ b/gcp-auth-extension/src/test/java/io/opentelemetry/contrib/gcp/auth/springapp/Application.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.gcp.auth.springapp; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +@SuppressWarnings("PrivateConstructorForUtilityClass") +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/gcp-auth-extension/src/test/java/io/opentelemetry/contrib/gcp/auth/springapp/Controller.java b/gcp-auth-extension/src/test/java/io/opentelemetry/contrib/gcp/auth/springapp/Controller.java new file mode 100644 index 000000000..95f90b35a --- /dev/null +++ b/gcp-auth-extension/src/test/java/io/opentelemetry/contrib/gcp/auth/springapp/Controller.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.gcp.auth.springapp; + +import io.opentelemetry.instrumentation.annotations.WithSpan; +import java.time.Duration; +import java.time.Instant; +import java.util.Random; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class Controller { + + private final Random random = new Random(); + + @GetMapping("/ping") + public String ping() { + int busyTime = random.nextInt(200); + busyloop(busyTime); + return "pong"; + } + + @WithSpan + private static long busyloop(int busyMillis) { + Instant start = Instant.now(); + Instant end; + long counter = 0; + do { + counter++; + end = Instant.now(); + } while (Duration.between(start, end).toMillis() < busyMillis); + return counter; + } +} diff --git a/gcp-auth-extension/src/test/resources/fakecreds.json b/gcp-auth-extension/src/test/resources/fakecreds.json new file mode 100644 index 000000000..1000f70db --- /dev/null +++ b/gcp-auth-extension/src/test/resources/fakecreds.json @@ -0,0 +1,13 @@ +{ + "type": "service_account", + "project_id": "quota-project-id", + "private_key_id": "aljmafmlamlmmasma", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALX0PQoe1igW12ikv1bN/r9lN749y2ijmbc/mFHPyS3hNTyOCjDvBbXYbDhQJzWVUikh4mvGBA07qTj79Xc3yBDfKP2IeyYQIFe0t0zkd7R9Zdn98Y2rIQC47aAbDfubtkU1U72t4zL11kHvoa0/RuFZjncvlr42X7be7lYh4p3NAgMBAAECgYASk5wDw4Az2ZkmeuN6Fk/y9H+Lcb2pskJIXjrL533vrDWGOC48LrsThMQPv8cxBky8HFSEklPpkfTF95tpD43iVwJRB/GrCtGTw65IfJ4/tI09h6zGc4yqvIo1cHX/LQ+SxKLGyir/dQM925rGt/VojxY5ryJR7GLbCzxPnJm/oQJBANwOCO6D2hy1LQYJhXh7O+RLtA/tSnT1xyMQsGT+uUCMiKS2bSKx2wxo9k7h3OegNJIu1q6nZ6AbxDK8H3+d0dUCQQDTrPSXagBxzp8PecbaCHjzNRSQE2in81qYnrAFNB4o3DpHyMMY6s5ALLeHKscEWnqP8Ur6X4PvzZecCWU9BKAZAkAutLPknAuxSCsUOvUfS1i87ex77Ot+w6POp34pEX+UWb+u5iFn2cQacDTHLV1LtE80L8jVLSbrbrlH43H0DjU5AkEAgidhycxS86dxpEljnOMCw8CKoUBd5I880IUahEiUltk7OLJYS/Ts1wbn3kPOVX3wyJs8WBDtBkFrDHW2ezth2QJADj3e1YhMVdjJW5jqwlD/VNddGjgzyunmiZg0uOXsHXbytYmsA545S8KRQFaJKFXYYFo2kOjqOiC1T2cAzMDjCQ==\n-----END PRIVATE KEY-----\n", + "client_email": "sample@appspot.gserviceaccount.com", + "client_id": "100000000000000000221", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "", + "client_x509_cert_url": "", + "universe_domain": "googleapis.com" +} diff --git a/gcp-auth-extension/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/gcp-auth-extension/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 000000000..1f0955d45 --- /dev/null +++ b/gcp-auth-extension/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline diff --git a/settings.gradle.kts b/settings.gradle.kts index 94edc1103..b06d458d0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -62,3 +62,4 @@ include(":gcp-resources") include(":span-stacktrace") include(":inferred-spans") include(":opamp-client") +include(":gcp-auth-extension")