Convert kubernetes-client-7.0 unit tests to Java (#9163)
This commit is contained in:
parent
0c79f14c79
commit
a332eb2887
|
@ -1,87 +0,0 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import io.opentelemetry.javaagent.instrumentation.kubernetesclient.KubernetesRequestDigest
|
||||
import io.opentelemetry.javaagent.instrumentation.kubernetesclient.KubernetesResource
|
||||
import io.opentelemetry.javaagent.instrumentation.kubernetesclient.KubernetesVerb
|
||||
import spock.lang.Specification
|
||||
|
||||
class KubernetesRequestUtilsTest extends Specification {
|
||||
def "asserting non-resource requests should work"() {
|
||||
expect:
|
||||
!KubernetesRequestDigest.isResourceRequest("/api")
|
||||
!KubernetesRequestDigest.isResourceRequest("/apis")
|
||||
!KubernetesRequestDigest.isResourceRequest("/apis/v1")
|
||||
!KubernetesRequestDigest.isResourceRequest("/healthz")
|
||||
!KubernetesRequestDigest.isResourceRequest("/swagger.json")
|
||||
!KubernetesRequestDigest.isResourceRequest("/api/v1")
|
||||
!KubernetesRequestDigest.isResourceRequest("/api/v1/")
|
||||
!KubernetesRequestDigest.isResourceRequest("/apis/apps/v1")
|
||||
!KubernetesRequestDigest.isResourceRequest("/apis/apps/v1/")
|
||||
}
|
||||
|
||||
def "asserting resource requests should work"() {
|
||||
expect:
|
||||
KubernetesRequestDigest.isResourceRequest("/apis/example.io/v1/foos")
|
||||
KubernetesRequestDigest.isResourceRequest("/apis/example.io/v1/namespaces/default/foos")
|
||||
KubernetesRequestDigest.isResourceRequest("/api/v1/namespaces")
|
||||
KubernetesRequestDigest.isResourceRequest("/api/v1/pods")
|
||||
KubernetesRequestDigest.isResourceRequest("/api/v1/namespaces/default/pods")
|
||||
}
|
||||
|
||||
def "parsing core resource from url-path should work"(String urlPath, String apiGroup, String apiVersion, String resource, String subResource, String namespace, String name) {
|
||||
expect:
|
||||
KubernetesResource.parseCoreResource(urlPath).apiGroup == apiGroup
|
||||
KubernetesResource.parseCoreResource(urlPath).apiVersion == apiVersion
|
||||
KubernetesResource.parseCoreResource(urlPath).resource == resource
|
||||
KubernetesResource.parseCoreResource(urlPath).subResource == subResource
|
||||
KubernetesResource.parseCoreResource(urlPath).namespace == namespace
|
||||
KubernetesResource.parseCoreResource(urlPath).name == name
|
||||
|
||||
where:
|
||||
urlPath | apiGroup | apiVersion | resource | subResource | namespace | name
|
||||
"/api/v1/pods" | "" | "v1" | "pods" | null | null | null
|
||||
"/api/v1/namespaces/default/pods" | "" | "v1" | "pods" | null | "default" | null
|
||||
"/api/v1/namespaces/default/pods/foo" | "" | "v1" | "pods" | null | "default" | "foo"
|
||||
"/api/v1/namespaces/default/pods/foo/exec" | "" | "v1" | "pods" | "exec" | "default" | "foo"
|
||||
}
|
||||
|
||||
def "parsing regular non-core resource from url-path should work"(String urlPath, String apiGroup, String apiVersion, String resource, String subResource, String namespace, String name) {
|
||||
expect:
|
||||
KubernetesResource.parseRegularResource(urlPath).apiGroup == apiGroup
|
||||
KubernetesResource.parseRegularResource(urlPath).apiVersion == apiVersion
|
||||
KubernetesResource.parseRegularResource(urlPath).resource == resource
|
||||
KubernetesResource.parseRegularResource(urlPath).subResource == subResource
|
||||
KubernetesResource.parseRegularResource(urlPath).namespace == namespace
|
||||
KubernetesResource.parseRegularResource(urlPath).name == name
|
||||
|
||||
where:
|
||||
urlPath | apiGroup | apiVersion | resource | subResource | namespace | name
|
||||
"/apis/apps/v1/deployments" | "apps" | "v1" | "deployments" | null | null | null
|
||||
"/apis/apps/v1/namespaces/default/deployments" | "apps" | "v1" | "deployments" | null | "default" | null
|
||||
"/apis/apps/v1/namespaces/default/deployments/foo" | "apps" | "v1" | "deployments" | null | "default" | "foo"
|
||||
"/apis/apps/v1/namespaces/default/deployments/foo/status" | "apps" | "v1" | "deployments" | "status" | "default" | "foo"
|
||||
"/apis/example.io/v1alpha1/foos" | "example.io" | "v1alpha1" | "foos" | null | null | null
|
||||
"/apis/example.io/v1alpha1/namespaces/default/foos" | "example.io" | "v1alpha1" | "foos" | null | "default" | null
|
||||
"/apis/example.io/v1alpha1/namespaces/default/foos/foo" | "example.io" | "v1alpha1" | "foos" | null | "default" | "foo"
|
||||
"/apis/example.io/v1alpha1/namespaces/default/foos/foo/status" | "example.io" | "v1alpha1" | "foos" | "status" | "default" | "foo"
|
||||
}
|
||||
|
||||
def "parsing kubernetes request verbs should work"(String httpVerb, boolean hasNamePathParam, boolean hasWatchParam, KubernetesVerb kubernetesVerb) {
|
||||
expect:
|
||||
KubernetesVerb.of(httpVerb, hasNamePathParam, hasWatchParam) == kubernetesVerb
|
||||
|
||||
where:
|
||||
httpVerb | hasNamePathParam | hasWatchParam | kubernetesVerb
|
||||
"GET" | true | false | KubernetesVerb.GET
|
||||
"GET" | false | true | KubernetesVerb.WATCH
|
||||
"GET" | false | false | KubernetesVerb.LIST
|
||||
"POST" | false | false | KubernetesVerb.CREATE
|
||||
"PUT" | false | false | KubernetesVerb.UPDATE
|
||||
"PATCH" | false | false | KubernetesVerb.PATCH
|
||||
"DELETE" | true | false | KubernetesVerb.DELETE
|
||||
"DELETE" | false | false | KubernetesVerb.DELETE_COLLECTION
|
||||
}
|
||||
}
|
|
@ -0,0 +1,192 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.javaagent.instrumentation.kubernetesclient;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtensionContext;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.ArgumentsProvider;
|
||||
import org.junit.jupiter.params.provider.ArgumentsSource;
|
||||
|
||||
class KubernetesRequestUtilsTest {
|
||||
|
||||
@Test
|
||||
void isResourceRequest() {
|
||||
assertThat(KubernetesRequestDigest.isResourceRequest("/api")).isFalse();
|
||||
assertThat(KubernetesRequestDigest.isResourceRequest("/apis")).isFalse();
|
||||
assertThat(KubernetesRequestDigest.isResourceRequest("/apis/v1")).isFalse();
|
||||
assertThat(KubernetesRequestDigest.isResourceRequest("/healthz")).isFalse();
|
||||
assertThat(KubernetesRequestDigest.isResourceRequest("/swagger.json")).isFalse();
|
||||
assertThat(KubernetesRequestDigest.isResourceRequest("/api/v1")).isFalse();
|
||||
assertThat(KubernetesRequestDigest.isResourceRequest("/api/v1/")).isFalse();
|
||||
assertThat(KubernetesRequestDigest.isResourceRequest("/apis/apps/v1")).isFalse();
|
||||
assertThat(KubernetesRequestDigest.isResourceRequest("/apis/apps/v1/")).isFalse();
|
||||
|
||||
assertThat(KubernetesRequestDigest.isResourceRequest("/apis/example.io/v1/foos")).isTrue();
|
||||
assertThat(
|
||||
KubernetesRequestDigest.isResourceRequest(
|
||||
"/apis/example.io/v1/namespaces/default/foos"))
|
||||
.isTrue();
|
||||
assertThat(KubernetesRequestDigest.isResourceRequest("/api/v1/namespaces")).isTrue();
|
||||
assertThat(KubernetesRequestDigest.isResourceRequest("/api/v1/pods")).isTrue();
|
||||
assertThat(KubernetesRequestDigest.isResourceRequest("/api/v1/namespaces/default/pods"))
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ArgumentsSource(ParseCoreResourceArgumentsProvider.class)
|
||||
void parseCoreResource(
|
||||
String urlPath,
|
||||
String apiGroup,
|
||||
String apiVersion,
|
||||
String resource,
|
||||
String subResource,
|
||||
String namespace,
|
||||
String name)
|
||||
throws ParseKubernetesResourceException {
|
||||
assertThat(KubernetesResource.parseCoreResource(urlPath).getApiGroup()).isEqualTo(apiGroup);
|
||||
assertThat(KubernetesResource.parseCoreResource(urlPath).getApiVersion()).isEqualTo(apiVersion);
|
||||
assertThat(KubernetesResource.parseCoreResource(urlPath).getResource()).isEqualTo(resource);
|
||||
assertThat(KubernetesResource.parseCoreResource(urlPath).getSubResource())
|
||||
.isEqualTo(subResource);
|
||||
assertThat(KubernetesResource.parseCoreResource(urlPath).getNamespace()).isEqualTo(namespace);
|
||||
assertThat(KubernetesResource.parseCoreResource(urlPath).getName()).isEqualTo(name);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ArgumentsSource(ParseRegularResourceArgumentsProvider.class)
|
||||
void parseRegularResource(
|
||||
String urlPath,
|
||||
String apiGroup,
|
||||
String apiVersion,
|
||||
String resource,
|
||||
String subResource,
|
||||
String namespace,
|
||||
String name)
|
||||
throws ParseKubernetesResourceException {
|
||||
assertThat(KubernetesResource.parseRegularResource(urlPath).getApiGroup()).isEqualTo(apiGroup);
|
||||
assertThat(KubernetesResource.parseRegularResource(urlPath).getApiVersion())
|
||||
.isEqualTo(apiVersion);
|
||||
assertThat(KubernetesResource.parseRegularResource(urlPath).getResource()).isEqualTo(resource);
|
||||
assertThat(KubernetesResource.parseRegularResource(urlPath).getSubResource())
|
||||
.isEqualTo(subResource);
|
||||
assertThat(KubernetesResource.parseRegularResource(urlPath).getNamespace())
|
||||
.isEqualTo(namespace);
|
||||
assertThat(KubernetesResource.parseRegularResource(urlPath).getName()).isEqualTo(name);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ArgumentsSource(K8sRequestVerbsArgumentsProvider.class)
|
||||
void k8sRequestVerbs(
|
||||
String httpVerb,
|
||||
boolean hasNamePathParam,
|
||||
boolean hasWatchParam,
|
||||
KubernetesVerb kubernetesVerb) {
|
||||
assertThat(KubernetesVerb.of(httpVerb, hasNamePathParam, hasWatchParam))
|
||||
.isEqualTo(kubernetesVerb);
|
||||
}
|
||||
|
||||
private static class K8sRequestVerbsArgumentsProvider implements ArgumentsProvider {
|
||||
|
||||
@Override
|
||||
public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext)
|
||||
throws Exception {
|
||||
return Stream.of(
|
||||
Arguments.of("GET", true, false, KubernetesVerb.GET),
|
||||
Arguments.of("GET", false, true, KubernetesVerb.WATCH),
|
||||
Arguments.of("GET", false, false, KubernetesVerb.LIST),
|
||||
Arguments.of("POST", false, false, KubernetesVerb.CREATE),
|
||||
Arguments.of("PUT", false, false, KubernetesVerb.UPDATE),
|
||||
Arguments.of("PATCH", false, false, KubernetesVerb.PATCH),
|
||||
Arguments.of("DELETE", true, false, KubernetesVerb.DELETE),
|
||||
Arguments.of("DELETE", false, false, KubernetesVerb.DELETE_COLLECTION));
|
||||
}
|
||||
}
|
||||
|
||||
private static class ParseRegularResourceArgumentsProvider implements ArgumentsProvider {
|
||||
|
||||
@Override
|
||||
public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext)
|
||||
throws Exception {
|
||||
return Stream.of(
|
||||
Arguments.of("/apis/apps/v1/deployments", "apps", "v1", "deployments", null, null, null),
|
||||
Arguments.of(
|
||||
"/apis/apps/v1/namespaces/default/deployments",
|
||||
"apps",
|
||||
"v1",
|
||||
"deployments",
|
||||
null,
|
||||
"default",
|
||||
null),
|
||||
Arguments.of(
|
||||
"/apis/apps/v1/namespaces/default/deployments/foo",
|
||||
"apps",
|
||||
"v1",
|
||||
"deployments",
|
||||
null,
|
||||
"default",
|
||||
"foo"),
|
||||
Arguments.of(
|
||||
"/apis/apps/v1/namespaces/default/deployments/foo/status",
|
||||
"apps",
|
||||
"v1",
|
||||
"deployments",
|
||||
"status",
|
||||
"default",
|
||||
"foo"),
|
||||
Arguments.of(
|
||||
"/apis/example.io/v1alpha1/foos", "example.io", "v1alpha1", "foos", null, null, null),
|
||||
Arguments.of(
|
||||
"/apis/example.io/v1alpha1/namespaces/default/foos",
|
||||
"example.io",
|
||||
"v1alpha1",
|
||||
"foos",
|
||||
null,
|
||||
"default",
|
||||
null),
|
||||
Arguments.of(
|
||||
"/apis/example.io/v1alpha1/namespaces/default/foos/foo",
|
||||
"example.io",
|
||||
"v1alpha1",
|
||||
"foos",
|
||||
null,
|
||||
"default",
|
||||
"foo"),
|
||||
Arguments.of(
|
||||
"/apis/example.io/v1alpha1/namespaces/default/foos/foo/status",
|
||||
"example.io",
|
||||
"v1alpha1",
|
||||
"foos",
|
||||
"status",
|
||||
"default",
|
||||
"foo"));
|
||||
}
|
||||
}
|
||||
|
||||
private static class ParseCoreResourceArgumentsProvider implements ArgumentsProvider {
|
||||
|
||||
@Override
|
||||
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
|
||||
return Stream.of(
|
||||
Arguments.of("/api/v1/pods", "", "v1", "pods", null, null, null),
|
||||
Arguments.of("/api/v1/namespaces/default/pods", "", "v1", "pods", null, "default", null),
|
||||
Arguments.of(
|
||||
"/api/v1/namespaces/default/pods/foo", "", "v1", "pods", null, "default", "foo"),
|
||||
Arguments.of(
|
||||
"/api/v1/namespaces/default/pods/foo/exec",
|
||||
"",
|
||||
"v1",
|
||||
"pods",
|
||||
"exec",
|
||||
"default",
|
||||
"foo"));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,222 +0,0 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import io.kubernetes.client.openapi.ApiCallback
|
||||
import io.kubernetes.client.openapi.ApiClient
|
||||
import io.kubernetes.client.openapi.ApiException
|
||||
import io.kubernetes.client.openapi.apis.CoreV1Api
|
||||
import io.opentelemetry.api.trace.SpanKind
|
||||
import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification
|
||||
import io.opentelemetry.instrumentation.test.asserts.TraceAssert
|
||||
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes
|
||||
import io.opentelemetry.testing.internal.armeria.common.HttpResponse
|
||||
import io.opentelemetry.testing.internal.armeria.common.HttpStatus
|
||||
import io.opentelemetry.testing.internal.armeria.common.MediaType
|
||||
import io.opentelemetry.testing.internal.armeria.testing.junit5.server.mock.MockWebServerExtension
|
||||
import spock.lang.Shared
|
||||
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
import static io.opentelemetry.api.trace.SpanKind.CLIENT
|
||||
import static io.opentelemetry.api.trace.StatusCode.ERROR
|
||||
|
||||
class KubernetesClientTest extends AgentInstrumentationSpecification {
|
||||
private static final String TEST_USER_AGENT = "test-user-agent"
|
||||
|
||||
@Shared
|
||||
def server = new MockWebServerExtension()
|
||||
|
||||
@Shared
|
||||
CoreV1Api api
|
||||
|
||||
def setupSpec() {
|
||||
server.start()
|
||||
def apiClient = new ApiClient()
|
||||
apiClient.setUserAgent(TEST_USER_AGENT)
|
||||
apiClient.basePath = server.httpUri().toString()
|
||||
api = new CoreV1Api(apiClient)
|
||||
}
|
||||
|
||||
def cleanupSpec() {
|
||||
server.stop()
|
||||
}
|
||||
|
||||
def setup() {
|
||||
server.beforeTestExecution(null)
|
||||
}
|
||||
|
||||
def "Kubernetes span is registered on a synchronous call"() {
|
||||
given:
|
||||
server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, "42"))
|
||||
|
||||
when:
|
||||
def response = runWithSpan("parent") {
|
||||
api.connectGetNamespacedPodProxy("name", "namespace", "path")
|
||||
}
|
||||
|
||||
then:
|
||||
response == "42"
|
||||
server.takeRequest().request().headers().get("traceparent") != null
|
||||
|
||||
assertTraces(1) {
|
||||
trace(0, 2) {
|
||||
span(0) {
|
||||
name "parent"
|
||||
kind SpanKind.INTERNAL
|
||||
hasNoParent()
|
||||
}
|
||||
apiClientSpan(it, 1, "get pods/proxy", "${server.httpUri()}/api/v1/namespaces/namespace/pods/name/proxy?path=path", 200)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def "Kubernetes instrumentation handles errors on a synchronous call"() {
|
||||
given:
|
||||
server.enqueue(HttpResponse.of(HttpStatus.valueOf(451), MediaType.PLAIN_TEXT_UTF_8, "42"))
|
||||
|
||||
when:
|
||||
runWithSpan("parent") {
|
||||
api.connectGetNamespacedPodProxy("name", "namespace", "path")
|
||||
}
|
||||
|
||||
then:
|
||||
def exception = thrown(ApiException)
|
||||
server.takeRequest().request().headers().get("traceparent") != null
|
||||
|
||||
assertTraces(1) {
|
||||
trace(0, 2) {
|
||||
span(0) {
|
||||
name "parent"
|
||||
kind SpanKind.INTERNAL
|
||||
hasNoParent()
|
||||
status ERROR
|
||||
errorEvent(exception.class, exception.message)
|
||||
}
|
||||
apiClientSpan(it, 1, "get pods/proxy", "${server.httpUri()}/api/v1/namespaces/namespace/pods/name/proxy?path=path", 451, exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def "Kubernetes span is registered on an asynchronous call"() {
|
||||
given:
|
||||
server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, "42"))
|
||||
|
||||
when:
|
||||
def responseBody = new AtomicReference<String>()
|
||||
def latch = new CountDownLatch(1)
|
||||
|
||||
runWithSpan("parent") {
|
||||
api.connectGetNamespacedPodProxyAsync("name", "namespace", "path", new ApiCallbackTemplate() {
|
||||
@Override
|
||||
void onSuccess(String result, int statusCode, Map<String, List<String>> responseHeaders) {
|
||||
responseBody.set(result)
|
||||
latch.countDown()
|
||||
runWithSpan("callback") {}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
then:
|
||||
latch.await()
|
||||
responseBody.get() == "42"
|
||||
server.takeRequest().request().headers().get("traceparent") != null
|
||||
|
||||
assertTraces(1) {
|
||||
trace(0, 3) {
|
||||
span(0) {
|
||||
name "parent"
|
||||
kind SpanKind.INTERNAL
|
||||
hasNoParent()
|
||||
}
|
||||
apiClientSpan(it, 1, "get pods/proxy", "${server.httpUri()}/api/v1/namespaces/namespace/pods/name/proxy?path=path", 200)
|
||||
span(2) {
|
||||
name "callback"
|
||||
kind SpanKind.INTERNAL
|
||||
childOf span(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def "Kubernetes instrumentation handles errors on an asynchronous call"() {
|
||||
given:
|
||||
server.enqueue(HttpResponse.of(HttpStatus.valueOf(451), MediaType.PLAIN_TEXT_UTF_8, "42"))
|
||||
|
||||
when:
|
||||
def exception = new AtomicReference<Exception>()
|
||||
def latch = new CountDownLatch(1)
|
||||
|
||||
runWithSpan("parent") {
|
||||
api.connectGetNamespacedPodProxyAsync("name", "namespace", "path", new ApiCallbackTemplate() {
|
||||
@Override
|
||||
void onFailure(ApiException e, int statusCode, Map<String, List<String>> responseHeaders) {
|
||||
exception.set(e)
|
||||
latch.countDown()
|
||||
runWithSpan("callback") {}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
then:
|
||||
latch.await()
|
||||
exception.get() != null
|
||||
server.takeRequest().request().headers().get("traceparent") != null
|
||||
|
||||
assertTraces(1) {
|
||||
trace(0, 3) {
|
||||
span(0) {
|
||||
name "parent"
|
||||
kind SpanKind.INTERNAL
|
||||
hasNoParent()
|
||||
}
|
||||
apiClientSpan(it, 1, "get pods/proxy", "${server.httpUri()}/api/v1/namespaces/namespace/pods/name/proxy?path=path", 451, exception.get())
|
||||
span(2) {
|
||||
name "callback"
|
||||
kind SpanKind.INTERNAL
|
||||
childOf span(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void apiClientSpan(TraceAssert trace, int index, String spanName, String url, int statusCode, Throwable exception = null) {
|
||||
boolean hasFailed = exception != null
|
||||
trace.span(index) {
|
||||
name spanName
|
||||
kind CLIENT
|
||||
childOf trace.span(0)
|
||||
if (hasFailed) {
|
||||
status ERROR
|
||||
errorEvent exception.class, exception.message
|
||||
}
|
||||
attributes {
|
||||
"$SemanticAttributes.HTTP_URL" url
|
||||
"$SemanticAttributes.HTTP_METHOD" "GET"
|
||||
"$SemanticAttributes.USER_AGENT_ORIGINAL" TEST_USER_AGENT
|
||||
"$SemanticAttributes.HTTP_STATUS_CODE" statusCode
|
||||
"$SemanticAttributes.NET_PEER_NAME" "127.0.0.1"
|
||||
"$SemanticAttributes.NET_PEER_PORT" server.httpPort()
|
||||
"$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long
|
||||
"kubernetes-client.namespace" "namespace"
|
||||
"kubernetes-client.name" "name"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class ApiCallbackTemplate implements ApiCallback<String> {
|
||||
@Override
|
||||
void onFailure(ApiException e, int statusCode, Map<String, List<String>> responseHeaders) {}
|
||||
|
||||
@Override
|
||||
void onSuccess(String result, int statusCode, Map<String, List<String>> responseHeaders) {}
|
||||
|
||||
@Override
|
||||
void onUploadProgress(long bytesWritten, long contentLength, boolean done) {}
|
||||
|
||||
@Override
|
||||
void onDownloadProgress(long bytesRead, long contentLength, boolean done) {}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,291 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.javaagent.instrumentation.kubernetesclient;
|
||||
|
||||
import static io.opentelemetry.instrumentation.testing.util.TelemetryDataUtil.orderByRootSpanName;
|
||||
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
|
||||
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import io.kubernetes.client.openapi.ApiCallback;
|
||||
import io.kubernetes.client.openapi.ApiClient;
|
||||
import io.kubernetes.client.openapi.ApiException;
|
||||
import io.kubernetes.client.openapi.apis.CoreV1Api;
|
||||
import io.opentelemetry.api.common.AttributeKey;
|
||||
import io.opentelemetry.api.trace.SpanKind;
|
||||
import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
|
||||
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
|
||||
import io.opentelemetry.sdk.trace.data.StatusData;
|
||||
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
|
||||
import io.opentelemetry.testing.internal.armeria.common.HttpResponse;
|
||||
import io.opentelemetry.testing.internal.armeria.common.HttpStatus;
|
||||
import io.opentelemetry.testing.internal.armeria.common.MediaType;
|
||||
import io.opentelemetry.testing.internal.armeria.testing.junit5.server.mock.MockWebServerExtension;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.assertj.core.api.AbstractAssert;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
|
||||
class KubernetesClientTest {
|
||||
|
||||
private static final String TEST_USER_AGENT = "test-user-agent";
|
||||
|
||||
@RegisterExtension
|
||||
private static final InstrumentationExtension testing = AgentInstrumentationExtension.create();
|
||||
|
||||
private final MockWebServerExtension mockWebServer = new MockWebServerExtension();
|
||||
|
||||
private CoreV1Api coreV1Api;
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
mockWebServer.start();
|
||||
ApiClient apiClient = new ApiClient();
|
||||
apiClient.setUserAgent(TEST_USER_AGENT);
|
||||
apiClient.setBasePath(mockWebServer.httpUri().toString());
|
||||
coreV1Api = new CoreV1Api(apiClient);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void afterEach() {
|
||||
mockWebServer.stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
void synchronousCall() throws ApiException {
|
||||
mockWebServer.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, "42"));
|
||||
String response =
|
||||
testing.runWithSpan(
|
||||
"parent", () -> coreV1Api.connectGetNamespacedPodProxy("name", "namespace", "path"));
|
||||
|
||||
assertThat(response).isEqualTo("42");
|
||||
assertThat(mockWebServer.takeRequest().request().headers().get("traceparent")).isNotBlank();
|
||||
|
||||
testing.waitAndAssertTraces(
|
||||
trace ->
|
||||
trace.hasSpansSatisfyingExactly(
|
||||
span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(),
|
||||
span ->
|
||||
span.hasName("get pods/proxy")
|
||||
.hasKind(SpanKind.CLIENT)
|
||||
.hasParent(trace.getSpan(0))
|
||||
.hasAttributesSatisfyingExactly(
|
||||
equalTo(
|
||||
SemanticAttributes.HTTP_URL,
|
||||
mockWebServer.httpUri()
|
||||
+ "/api/v1/namespaces/namespace/pods/name/proxy?path=path"),
|
||||
equalTo(SemanticAttributes.HTTP_METHOD, "GET"),
|
||||
equalTo(SemanticAttributes.USER_AGENT_ORIGINAL, TEST_USER_AGENT),
|
||||
equalTo(SemanticAttributes.HTTP_STATUS_CODE, 200),
|
||||
equalTo(SemanticAttributes.NET_PEER_NAME, "127.0.0.1"),
|
||||
equalTo(SemanticAttributes.NET_PEER_PORT, mockWebServer.httpPort()),
|
||||
satisfies(
|
||||
SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH,
|
||||
AbstractAssert::isNotNull),
|
||||
equalTo(
|
||||
AttributeKey.stringKey("kubernetes-client.namespace"), "namespace"),
|
||||
equalTo(AttributeKey.stringKey("kubernetes-client.name"), "name"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void handleErrorsInSyncCall() {
|
||||
mockWebServer.enqueue(
|
||||
HttpResponse.of(HttpStatus.valueOf(451), MediaType.PLAIN_TEXT_UTF_8, "42"));
|
||||
AtomicReference<ApiException> apiExceptionReference = new AtomicReference<>(null);
|
||||
try {
|
||||
testing.runWithSpan(
|
||||
"parent",
|
||||
() -> {
|
||||
coreV1Api.connectGetNamespacedPodProxy("name", "namespace", "path");
|
||||
});
|
||||
} catch (ApiException e) {
|
||||
apiExceptionReference.set(e);
|
||||
}
|
||||
assertThat(apiExceptionReference.get()).isNotNull();
|
||||
assertThat(mockWebServer.takeRequest().request().headers().get("traceparent")).isNotBlank();
|
||||
|
||||
ApiException apiException = apiExceptionReference.get();
|
||||
|
||||
testing.waitAndAssertTraces(
|
||||
trace ->
|
||||
trace.hasSpansSatisfyingExactly(
|
||||
span ->
|
||||
span.hasName("parent")
|
||||
.hasKind(SpanKind.INTERNAL)
|
||||
.hasNoParent()
|
||||
.hasStatus(StatusData.error())
|
||||
.hasException(apiException),
|
||||
span ->
|
||||
span.hasName("get pods/proxy")
|
||||
.hasKind(SpanKind.CLIENT)
|
||||
.hasParent(trace.getSpan(0))
|
||||
.hasStatus(StatusData.error())
|
||||
.hasException(apiException)
|
||||
.hasAttributesSatisfyingExactly(
|
||||
equalTo(
|
||||
SemanticAttributes.HTTP_URL,
|
||||
mockWebServer.httpUri()
|
||||
+ "/api/v1/namespaces/namespace/pods/name/proxy?path=path"),
|
||||
equalTo(SemanticAttributes.HTTP_METHOD, "GET"),
|
||||
equalTo(SemanticAttributes.USER_AGENT_ORIGINAL, TEST_USER_AGENT),
|
||||
equalTo(SemanticAttributes.HTTP_STATUS_CODE, 451),
|
||||
equalTo(SemanticAttributes.NET_PEER_NAME, "127.0.0.1"),
|
||||
equalTo(SemanticAttributes.NET_PEER_PORT, mockWebServer.httpPort()),
|
||||
satisfies(
|
||||
SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH,
|
||||
AbstractAssert::isNotNull),
|
||||
equalTo(
|
||||
AttributeKey.stringKey("kubernetes-client.namespace"), "namespace"),
|
||||
equalTo(AttributeKey.stringKey("kubernetes-client.name"), "name"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void asynchronousCall() throws ApiException, InterruptedException {
|
||||
mockWebServer.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, "42"));
|
||||
|
||||
AtomicReference<String> responseBodyReference = new AtomicReference<>();
|
||||
CountDownLatch countDownLatch = new CountDownLatch(1);
|
||||
|
||||
testing.runWithSpan(
|
||||
"parent",
|
||||
() -> {
|
||||
coreV1Api.connectGetNamespacedPodProxyAsync(
|
||||
"name",
|
||||
"namespace",
|
||||
"path",
|
||||
new ApiCallbackTemplate() {
|
||||
@Override
|
||||
public void onSuccess(
|
||||
String result, int statusCode, Map<String, List<String>> responseHeaders) {
|
||||
responseBodyReference.set(result);
|
||||
countDownLatch.countDown();
|
||||
testing.runWithSpan("callback", () -> {});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
countDownLatch.await();
|
||||
|
||||
assertThat(responseBodyReference.get()).isEqualTo("42");
|
||||
assertThat(mockWebServer.takeRequest().request().headers().get("traceparent")).isNotBlank();
|
||||
|
||||
testing.waitAndAssertSortedTraces(
|
||||
orderByRootSpanName("parent", "get pods/proxy", "callback"),
|
||||
trace ->
|
||||
trace.hasSpansSatisfyingExactly(
|
||||
span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(),
|
||||
span ->
|
||||
span.hasName("get pods/proxy")
|
||||
.hasKind(SpanKind.CLIENT)
|
||||
.hasParent(trace.getSpan(0))
|
||||
.hasAttributesSatisfyingExactly(
|
||||
equalTo(
|
||||
SemanticAttributes.HTTP_URL,
|
||||
mockWebServer.httpUri()
|
||||
+ "/api/v1/namespaces/namespace/pods/name/proxy?path=path"),
|
||||
equalTo(SemanticAttributes.HTTP_METHOD, "GET"),
|
||||
equalTo(SemanticAttributes.USER_AGENT_ORIGINAL, TEST_USER_AGENT),
|
||||
equalTo(SemanticAttributes.HTTP_STATUS_CODE, 200),
|
||||
equalTo(SemanticAttributes.NET_PEER_NAME, "127.0.0.1"),
|
||||
equalTo(SemanticAttributes.NET_PEER_PORT, mockWebServer.httpPort()),
|
||||
satisfies(
|
||||
SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH,
|
||||
AbstractAssert::isNotNull),
|
||||
equalTo(
|
||||
AttributeKey.stringKey("kubernetes-client.namespace"), "namespace"),
|
||||
equalTo(AttributeKey.stringKey("kubernetes-client.name"), "name")),
|
||||
span ->
|
||||
span.hasName("callback")
|
||||
.hasKind(SpanKind.INTERNAL)
|
||||
.hasParent(trace.getSpan(0))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void handleErrorsInAsynchronousCall() throws ApiException, InterruptedException {
|
||||
|
||||
mockWebServer.enqueue(
|
||||
HttpResponse.of(HttpStatus.valueOf(451), MediaType.PLAIN_TEXT_UTF_8, "42"));
|
||||
|
||||
AtomicReference<ApiException> exceptionReference = new AtomicReference<>();
|
||||
CountDownLatch countDownLatch = new CountDownLatch(1);
|
||||
|
||||
testing.runWithSpan(
|
||||
"parent",
|
||||
() -> {
|
||||
coreV1Api.connectGetNamespacedPodProxyAsync(
|
||||
"name",
|
||||
"namespace",
|
||||
"path",
|
||||
new ApiCallbackTemplate() {
|
||||
@Override
|
||||
public void onFailure(
|
||||
ApiException e, int statusCode, Map<String, List<String>> responseHeaders) {
|
||||
exceptionReference.set(e);
|
||||
countDownLatch.countDown();
|
||||
testing.runWithSpan("callback", () -> {});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
countDownLatch.await();
|
||||
|
||||
assertThat(exceptionReference.get()).isNotNull();
|
||||
assertThat(mockWebServer.takeRequest().request().headers().get("traceparent")).isNotBlank();
|
||||
|
||||
testing.waitAndAssertSortedTraces(
|
||||
orderByRootSpanName("parent", "get pods/proxy", "callback"),
|
||||
trace ->
|
||||
trace.hasSpansSatisfyingExactly(
|
||||
span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(),
|
||||
span ->
|
||||
span.hasName("get pods/proxy")
|
||||
.hasKind(SpanKind.CLIENT)
|
||||
.hasParent(trace.getSpan(0))
|
||||
.hasStatus(StatusData.error())
|
||||
.hasException(exceptionReference.get())
|
||||
.hasAttributesSatisfyingExactly(
|
||||
equalTo(
|
||||
SemanticAttributes.HTTP_URL,
|
||||
mockWebServer.httpUri()
|
||||
+ "/api/v1/namespaces/namespace/pods/name/proxy?path=path"),
|
||||
equalTo(SemanticAttributes.HTTP_METHOD, "GET"),
|
||||
equalTo(SemanticAttributes.USER_AGENT_ORIGINAL, TEST_USER_AGENT),
|
||||
equalTo(SemanticAttributes.HTTP_STATUS_CODE, 451),
|
||||
equalTo(SemanticAttributes.NET_PEER_NAME, "127.0.0.1"),
|
||||
equalTo(SemanticAttributes.NET_PEER_PORT, mockWebServer.httpPort()),
|
||||
satisfies(
|
||||
SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH,
|
||||
AbstractAssert::isNotNull),
|
||||
equalTo(
|
||||
AttributeKey.stringKey("kubernetes-client.namespace"), "namespace"),
|
||||
equalTo(AttributeKey.stringKey("kubernetes-client.name"), "name")),
|
||||
span ->
|
||||
span.hasName("callback")
|
||||
.hasKind(SpanKind.INTERNAL)
|
||||
.hasParent(trace.getSpan(0))));
|
||||
}
|
||||
|
||||
private static class ApiCallbackTemplate implements ApiCallback<String> {
|
||||
@Override
|
||||
public void onFailure(
|
||||
ApiException e, int statusCode, Map<String, List<String>> responseHeaders) {}
|
||||
|
||||
@Override
|
||||
public void onSuccess(
|
||||
String result, int statusCode, Map<String, List<String>> responseHeaders) {}
|
||||
|
||||
@Override
|
||||
public void onUploadProgress(long bytesWritten, long contentLength, boolean done) {}
|
||||
|
||||
@Override
|
||||
public void onDownloadProgress(long bytesRead, long contentLength, boolean done) {}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue