Skip to content

Commit 8745020

Browse files
committed
Add integration smoke test
1 parent ae6a4b2 commit 8745020

File tree

5 files changed

+357
-1
lines changed

5 files changed

+357
-1
lines changed

gcp-auth-extension/build.gradle.kts

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,17 @@ plugins {
22
id("otel.java-conventions")
33
id("otel.publish-conventions")
44
id("com.github.johnrengelman.shadow")
5+
id("org.springframework.boot") version "2.7.18"
56
}
67

78
description = "OpenTelemetry Java Agent Extension that enables authentication support for OTLP exporters"
89
otelJava.moduleName.set("io.opentelemetry.contrib.gcp.auth")
910

11+
val agent: Configuration by configurations.creating {
12+
isCanBeResolved = true
13+
isCanBeConsumed = false
14+
}
15+
1016
dependencies {
1117
annotationProcessor("com.google.auto.service:auto-service")
1218
compileOnly("com.google.auto.service:auto-service-annotations")
@@ -29,12 +35,81 @@ dependencies {
2935
testImplementation("io.opentelemetry:opentelemetry-exporter-otlp")
3036
testImplementation("io.opentelemetry:opentelemetry-sdk-testing")
3137
testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
38+
testImplementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations")
3239

3340
testImplementation("org.awaitility:awaitility")
3441
testImplementation("org.mockito:mockito-inline")
3542
testImplementation("org.mockito:mockito-junit-jupiter")
43+
testImplementation("org.mock-server:mockserver-netty:5.15.0")
44+
testImplementation("io.opentelemetry.proto:opentelemetry-proto:1.4.0-alpha")
45+
testImplementation("org.springframework.boot:spring-boot-starter-web:2.7.18")
46+
testImplementation("org.springframework.boot:spring-boot-starter:2.7.18")
47+
testImplementation("org.springframework.boot:spring-boot-starter-test:2.7.18")
48+
49+
agent("io.opentelemetry.javaagent:opentelemetry-javaagent")
3650
}
3751

38-
tasks.test {
52+
tasks {
53+
test {
54+
useJUnitPlatform()
55+
// exclude integration test
56+
exclude("io/opentelemetry/contrib/gcp/auth/GcpAuthExtensionEndToEndTest.class")
57+
}
58+
59+
shadowJar {
60+
archiveFileName.set("gcp-auth-extension.jar")
61+
}
62+
63+
jar {
64+
// Disable standard jar
65+
enabled = false
66+
}
67+
68+
assemble {
69+
dependsOn(shadowJar)
70+
}
71+
72+
bootJar {
73+
// disable bootJar in build since it only runs as part of test
74+
enabled = false
75+
}
76+
}
77+
78+
val builtLibsDir = layout.buildDirectory.dir("libs").get().asFile.absolutePath
79+
val javaAgentJarPath = "$builtLibsDir/otel-agent.jar"
80+
val authExtensionJarPath = "$builtLibsDir/gcp-auth-extension.jar"
81+
82+
tasks.register<Copy>("copyAgent") {
83+
into(layout.buildDirectory.dir("libs"))
84+
from(configurations.named("agent") {
85+
rename("opentelemetry-javaagent(.*).jar", "otel-agent.jar")
86+
})
87+
}
88+
89+
tasks.register<Test>("IntegrationTest") {
90+
dependsOn(tasks.shadowJar)
91+
dependsOn(tasks.named("copyAgent"))
92+
3993
useJUnitPlatform()
94+
// include only the integration test file
95+
include("io/opentelemetry/contrib/gcp/auth/GcpAuthExtensionEndToEndTest.class")
96+
97+
val fakeCredsFilePath = project.file("src/test/resources/fakecreds.json").absolutePath
98+
99+
environment("GOOGLE_CLOUD_QUOTA_PROJECT", "quota-project-id")
100+
environment("GOOGLE_APPLICATION_CREDENTIALS", fakeCredsFilePath)
101+
jvmArgs = listOf(
102+
"-javaagent:$javaAgentJarPath",
103+
"-Dotel.javaagent.extensions=$authExtensionJarPath",
104+
"-Dgoogle.cloud.project=my-gcp-project",
105+
"-Dotel.java.global-autoconfigure.enabled=true",
106+
"-Dotel.exporter.otlp.endpoint=http://localhost:4318",
107+
"-Dotel.resource.providers.gcp.enabled=true",
108+
"-Dotel.traces.exporter=otlp",
109+
"-Dotel.bsp.schedule.delay=2000",
110+
"-Dotel.metrics.exporter=none",
111+
"-Dotel.logs.exporter=none",
112+
"-Dotel.exporter.otlp.protocol=http/protobuf",
113+
"-Dmockserver.logLevel=off"
114+
)
40115
}
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.gcp.auth;
7+
8+
import static io.opentelemetry.contrib.gcp.auth.GcpAuthAutoConfigurationCustomizerProvider.GCP_USER_PROJECT_ID_KEY;
9+
import static io.opentelemetry.contrib.gcp.auth.GcpAuthAutoConfigurationCustomizerProvider.QUOTA_USER_PROJECT_HEADER;
10+
import static org.awaitility.Awaitility.await;
11+
import static org.junit.jupiter.api.Assertions.assertFalse;
12+
import static org.junit.jupiter.api.Assertions.assertTrue;
13+
import static org.mockserver.model.HttpRequest.request;
14+
import static org.mockserver.model.HttpResponse.response;
15+
import static org.mockserver.stop.Stop.stopQuietly;
16+
17+
import com.google.protobuf.InvalidProtocolBufferException;
18+
import io.opentelemetry.contrib.gcp.auth.springapp.Application;
19+
import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest;
20+
import io.opentelemetry.proto.common.v1.AnyValue;
21+
import io.opentelemetry.proto.common.v1.KeyValue;
22+
import io.opentelemetry.proto.trace.v1.ResourceSpans;
23+
import java.net.URI;
24+
import java.security.KeyManagementException;
25+
import java.security.NoSuchAlgorithmException;
26+
import java.security.cert.X509Certificate;
27+
import java.time.Duration;
28+
import java.util.Arrays;
29+
import java.util.List;
30+
import java.util.Optional;
31+
import java.util.stream.Collectors;
32+
import javax.net.ssl.HttpsURLConnection;
33+
import javax.net.ssl.SSLContext;
34+
import javax.net.ssl.TrustManager;
35+
import javax.net.ssl.X509TrustManager;
36+
import org.junit.jupiter.api.AfterAll;
37+
import org.junit.jupiter.api.BeforeAll;
38+
import org.junit.jupiter.api.Test;
39+
import org.mockserver.client.MockServerClient;
40+
import org.mockserver.integration.ClientAndServer;
41+
import org.mockserver.model.Body;
42+
import org.mockserver.model.Headers;
43+
import org.mockserver.model.HttpRequest;
44+
import org.mockserver.model.JsonBody;
45+
import org.springframework.beans.factory.annotation.Autowired;
46+
import org.springframework.boot.test.context.SpringBootTest;
47+
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
48+
import org.springframework.boot.test.web.client.TestRestTemplate;
49+
import org.springframework.boot.test.web.server.LocalServerPort;
50+
51+
@SpringBootTest(
52+
classes = {Application.class},
53+
webEnvironment = WebEnvironment.RANDOM_PORT)
54+
public class GcpAuthExtensionEndToEndTest {
55+
56+
@LocalServerPort private int testApplicationPort; // port at which the spring app is running
57+
58+
@Autowired private TestRestTemplate template;
59+
60+
// The port at which the backend server will receive telemetry
61+
private static final int EXPORTER_ENDPOINT_PORT = 4318;
62+
// The port at which the mock GCP OAuth 2.0 server will run
63+
private static final int MOCK_GCP_OAUTH2_PORT = 8090;
64+
65+
// Backend server to which the application under test will export traces
66+
// the export config is specified in the build.gradle file.
67+
private static ClientAndServer backendServer;
68+
69+
// Mock server to intercept calls to the GCP OAuth 2.0 server and provide fake credentials
70+
private static ClientAndServer mockGcpOAuth2Server;
71+
72+
private static final String DUMMY_GCP_QUOTA_PROJECT = System.getenv("GOOGLE_CLOUD_QUOTA_PROJECT");
73+
private static final String DUMMY_GCP_PROJECT = System.getProperty("google.cloud.project");
74+
75+
@BeforeAll
76+
public static void setup() throws NoSuchAlgorithmException, KeyManagementException {
77+
// Setup proxy host(s)
78+
System.setProperty("http.proxyHost", "localhost");
79+
System.setProperty("http.proxyPort", MOCK_GCP_OAUTH2_PORT + "");
80+
System.setProperty("https.proxyHost", "localhost");
81+
System.setProperty("https.proxyPort", MOCK_GCP_OAUTH2_PORT + "");
82+
System.setProperty("http.nonProxyHost", "localhost");
83+
System.setProperty("https.nonProxyHost", "localhost");
84+
85+
// Disable SSL validation for integration test
86+
// The OAuth2 token validation requires SSL validation
87+
disableSSLValidation();
88+
89+
// Set up mock OTLP backend server to which traces will be exported
90+
backendServer = ClientAndServer.startClientAndServer(EXPORTER_ENDPOINT_PORT);
91+
backendServer.when(request()).respond(response().withStatusCode(200));
92+
93+
// Set up the mock gcp metadata server to provide fake credentials
94+
String accessTokenResponse =
95+
"{\"access_token\": \"fake.access_token\",\"expires_in\": 3600, \"token_type\": \"Bearer\"}";
96+
mockGcpOAuth2Server = ClientAndServer.startClientAndServer(MOCK_GCP_OAUTH2_PORT);
97+
98+
MockServerClient mockServerClient =
99+
new MockServerClient("localhost", MOCK_GCP_OAUTH2_PORT).withSecure(true);
100+
101+
// mock the token refresh - always respond with 200
102+
mockServerClient
103+
.when(request().withMethod("POST").withPath("/token"))
104+
.respond(
105+
response()
106+
.withStatusCode(200)
107+
.withHeader("Content-Type", "application/json")
108+
.withBody(new JsonBody(accessTokenResponse)));
109+
}
110+
111+
@AfterAll
112+
public static void teardown() {
113+
// Stop the backend server
114+
stopQuietly(backendServer);
115+
stopQuietly(mockGcpOAuth2Server);
116+
}
117+
118+
@Test
119+
public void authExtensionSmokeTest() {
120+
template.getForEntity(
121+
URI.create("http://localhost:" + testApplicationPort + "/ping"), String.class);
122+
123+
await()
124+
.atMost(Duration.ofSeconds(10))
125+
.untilAsserted(
126+
() -> {
127+
HttpRequest[] requests = backendServer.retrieveRecordedRequests(request());
128+
List<Headers> extractedHeaders = extractHeadersFromRequests(requests);
129+
verifyRequestHeaders(extractedHeaders);
130+
131+
List<ResourceSpans> extractedResourceSpans =
132+
extractResourceSpansFromRequests(requests);
133+
verifyResourceAttributes(extractedResourceSpans);
134+
});
135+
}
136+
137+
// Helper methods
138+
139+
private static void disableSSLValidation()
140+
throws NoSuchAlgorithmException, KeyManagementException {
141+
TrustManager[] trustAllCerts =
142+
new TrustManager[] {
143+
new X509TrustManager() {
144+
@Override
145+
public void checkClientTrusted(X509Certificate[] chain, String authType) {}
146+
147+
@Override
148+
public void checkServerTrusted(X509Certificate[] chain, String authType) {}
149+
150+
@Override
151+
public X509Certificate[] getAcceptedIssuers() {
152+
return null;
153+
}
154+
}
155+
};
156+
SSLContext sc = SSLContext.getInstance("SSL");
157+
sc.init(null, trustAllCerts, new java.security.SecureRandom());
158+
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
159+
}
160+
161+
private static void verifyResourceAttributes(List<ResourceSpans> extractedResourceSpans) {
162+
extractedResourceSpans.forEach(
163+
resourceSpan ->
164+
assertTrue(
165+
resourceSpan
166+
.getResource()
167+
.getAttributesList()
168+
.contains(
169+
KeyValue.newBuilder()
170+
.setKey(GCP_USER_PROJECT_ID_KEY)
171+
.setValue(AnyValue.newBuilder().setStringValue(DUMMY_GCP_PROJECT))
172+
.build())));
173+
}
174+
175+
private static void verifyRequestHeaders(List<Headers> extractedHeaders) {
176+
assertFalse(extractedHeaders.isEmpty());
177+
// verify if extension added the required headers
178+
extractedHeaders.forEach(
179+
headers -> {
180+
assertTrue(headers.containsEntry(QUOTA_USER_PROJECT_HEADER, DUMMY_GCP_QUOTA_PROJECT));
181+
assertTrue(headers.containsEntry("Authorization", "Bearer fake.access_token"));
182+
});
183+
}
184+
185+
private static List<Headers> extractHeadersFromRequests(HttpRequest[] requests) {
186+
return Arrays.stream(requests).map(HttpRequest::getHeaders).collect(Collectors.toList());
187+
}
188+
189+
/**
190+
* Extract resource spans from http requests received by a telemetry collector.
191+
*
192+
* @param requests Request received by a http server trace collector
193+
* @return spans extracted from the request body
194+
*/
195+
private static List<ResourceSpans> extractResourceSpansFromRequests(HttpRequest[] requests) {
196+
return Arrays.stream(requests)
197+
.map(HttpRequest::getBody)
198+
.map(GcpAuthExtensionEndToEndTest::getExportTraceServiceRequest)
199+
.filter(Optional::isPresent)
200+
.map(Optional::get)
201+
.flatMap(
202+
exportTraceServiceRequest -> exportTraceServiceRequest.getResourceSpansList().stream())
203+
.collect(Collectors.toList());
204+
}
205+
206+
private static Optional<ExportTraceServiceRequest> getExportTraceServiceRequest(Body<?> body) {
207+
try {
208+
return Optional.ofNullable(ExportTraceServiceRequest.parseFrom(body.getRawBytes()));
209+
} catch (InvalidProtocolBufferException e) {
210+
return Optional.empty();
211+
}
212+
}
213+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.gcp.auth.springapp;
7+
8+
import org.springframework.boot.SpringApplication;
9+
import org.springframework.boot.autoconfigure.SpringBootApplication;
10+
11+
@SpringBootApplication
12+
@SuppressWarnings("PrivateConstructorForUtilityClass")
13+
public class Application {
14+
public static void main(String[] args) {
15+
SpringApplication.run(Application.class, args);
16+
}
17+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.gcp.auth.springapp;
7+
8+
import io.opentelemetry.instrumentation.annotations.WithSpan;
9+
import java.time.Duration;
10+
import java.time.Instant;
11+
import java.util.Random;
12+
import org.springframework.web.bind.annotation.GetMapping;
13+
import org.springframework.web.bind.annotation.RestController;
14+
15+
@RestController
16+
public class Controller {
17+
18+
private final Random random = new Random();
19+
20+
@GetMapping("/ping")
21+
public String ping() {
22+
int busyTime = random.nextInt(200);
23+
busyloop(busyTime);
24+
return "pong";
25+
}
26+
27+
@WithSpan
28+
private static long busyloop(int busyMillis) {
29+
Instant start = Instant.now();
30+
Instant end;
31+
long counter = 0;
32+
do {
33+
counter++;
34+
end = Instant.now();
35+
} while (Duration.between(start, end).toMillis() < busyMillis);
36+
return counter;
37+
}
38+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"type": "service_account",
3+
"project_id": "quota-project-id",
4+
"private_key_id": "aljmafmlamlmmasma",
5+
"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",
6+
"client_email": "[email protected]",
7+
"client_id": "100000000000000000221",
8+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
9+
"token_uri": "https://oauth2.googleapis.com/token",
10+
"auth_provider_x509_cert_url": "",
11+
"client_x509_cert_url": "",
12+
"universe_domain": "googleapis.com"
13+
}

0 commit comments

Comments
 (0)