diff --git a/communication/src/main/java/datadog/communication/BackendApiFactory.java b/communication/src/main/java/datadog/communication/BackendApiFactory.java index eeb960b3a70..bc54da66460 100644 --- a/communication/src/main/java/datadog/communication/BackendApiFactory.java +++ b/communication/src/main/java/datadog/communication/BackendApiFactory.java @@ -1,6 +1,8 @@ package datadog.communication; import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static java.util.Collections.unmodifiableMap; import datadog.communication.ddagent.DDAgentFeaturesDiscovery; import datadog.communication.ddagent.SharedCommunicationObjects; @@ -8,9 +10,12 @@ import datadog.trace.api.Config; import datadog.trace.api.intake.Intake; import datadog.trace.util.throwable.FatalAgentMisconfigurationError; +import java.util.HashMap; +import java.util.Map; import javax.annotation.Nullable; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; +import okhttp3.Request; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,14 +27,21 @@ public class BackendApiFactory { private final Config config; private final SharedCommunicationObjects sharedCommunicationObjects; + private final Map requestHeaders; private final boolean sendOnce; public BackendApiFactory(Config config, SharedCommunicationObjects sharedCommunicationObjects) { - this(config, sharedCommunicationObjects, false); + this(config, sharedCommunicationObjects, emptyMap(), false); + } + + /** Creates a backend factory with optional send-once transport semantics and no extra headers. */ + public BackendApiFactory( + Config config, SharedCommunicationObjects sharedCommunicationObjects, boolean sendOnce) { + this(config, sharedCommunicationObjects, emptyMap(), sendOnce); } /** - * Creates a backend factory with optional send-once transport semantics. + * Creates a backend factory with per-request headers and optional send-once transport semantics. * *

When {@code sendOnce} is true, automatic connection retries and redirects are disabled, and * the default HTTP retry policy is {@link HttpRetryPolicy.Factory#NEVER_RETRY}. Explicit caller @@ -37,9 +49,13 @@ public BackendApiFactory(Config config, SharedCommunicationObjects sharedCommuni * payload without an idempotency key. */ public BackendApiFactory( - Config config, SharedCommunicationObjects sharedCommunicationObjects, boolean sendOnce) { + Config config, + SharedCommunicationObjects sharedCommunicationObjects, + Map requestHeaders, + boolean sendOnce) { this.config = config; this.sharedCommunicationObjects = sharedCommunicationObjects; + this.requestHeaders = unmodifiableMap(new HashMap<>(requestHeaders)); this.sendOnce = sendOnce; } @@ -182,7 +198,8 @@ private static boolean isAsciiLetterOrDigit(final char character) { * Creates an EVP proxy client after Agent discovery, optionally forcing a fresh discovery and * requiring the Agent to advertise every specified forwarding header. * - *

Callers supply the forwarding capabilities their event protocol requires. + *

Callers supply the forwarding capabilities their event protocol requires. These are + * independent of any headers configured on this factory. * *

The {@code forceDiscovery} form is intended for bounded unavailable-route recovery probes. */ @@ -250,14 +267,24 @@ private BackendApi createEvpProxyApi( } OkHttpClient configureHttpClient(final OkHttpClient httpClient) { - return sendOnce - ? httpClient - .newBuilder() - .retryOnConnectionFailure(false) - .followRedirects(false) - .followSslRedirects(false) - .build() - : httpClient; + if (requestHeaders.isEmpty() && !sendOnce) { + return httpClient; + } + final OkHttpClient.Builder builder = httpClient.newBuilder(); + if (sendOnce) { + builder.retryOnConnectionFailure(false).followRedirects(false).followSslRedirects(false); + } + if (!requestHeaders.isEmpty()) { + builder.addInterceptor( + chain -> { + final Request.Builder requestBuilder = chain.request().newBuilder(); + for (Map.Entry header : requestHeaders.entrySet()) { + requestBuilder.header(header.getKey(), header.getValue()); + } + return chain.proceed(requestBuilder.build()); + }); + } + return builder.build(); } private HttpRetryPolicy.Factory retryPolicyFactory() { diff --git a/communication/src/main/java/datadog/communication/EvpProxy.java b/communication/src/main/java/datadog/communication/EvpProxy.java index 301c48f0ff5..05ce9757339 100644 --- a/communication/src/main/java/datadog/communication/EvpProxy.java +++ b/communication/src/main/java/datadog/communication/EvpProxy.java @@ -11,6 +11,9 @@ public final class EvpProxy { /** Identifies the version of the SDK that produced an EVP request. */ public static final String ORIGIN_VERSION_HEADER = "DD-EVP-ORIGIN-VERSION"; + /** Origin header value identifying this tracing library. */ + public static final String JAVA_TRACING_LIBRARY = "dd-trace-java"; + /** * Default SDK-side target for uncompressed EVP request bodies. Writers may split batches at or * below this size to keep Agent proxy requests comfortably bounded. diff --git a/communication/src/test/java/datadog/communication/BackendApiFactoryTest.java b/communication/src/test/java/datadog/communication/BackendApiFactoryTest.java index 155b2087789..de564f46ce5 100644 --- a/communication/src/test/java/datadog/communication/BackendApiFactoryTest.java +++ b/communication/src/test/java/datadog/communication/BackendApiFactoryTest.java @@ -1,10 +1,14 @@ package datadog.communication; +import static datadog.communication.EvpProxy.JAVA_TRACING_LIBRARY; import static datadog.communication.EvpProxy.ORIGIN_HEADER; import static datadog.communication.EvpProxy.ORIGIN_VERSION_HEADER; import static datadog.communication.ddagent.DDAgentFeaturesDiscovery.V2_EVP_PROXY_ENDPOINT; import static datadog.communication.ddagent.DDAgentFeaturesDiscovery.V4_EVP_PROXY_ENDPOINT; +import static datadog.trace.api.config.CiVisibilityConfig.CIVISIBILITY_AGENTLESS_URL; +import static datadog.trace.api.config.GeneralConfig.API_KEY; import static java.util.Arrays.asList; +import static java.util.Collections.singletonMap; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -22,7 +26,10 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.HashMap; import java.util.Locale; +import java.util.Map; +import java.util.Properties; import java.util.stream.Stream; import okhttp3.HttpUrl; import okhttp3.MediaType; @@ -327,7 +334,7 @@ void discoveryRejectsMissingOrPartialIdentityCapabilities(final String capabilit } @Test - void featureFlagProxyRequestNeverCarriesDirectCredentials() throws Exception { + void featureFlagProxyRequestCarriesSdkIdentityWithoutDirectCredentials() throws Exception { final MockWebServer agent = new MockWebServer(); agent.enqueue(new MockResponse().setResponseCode(200).setBody("{}")); agent.start(); @@ -335,7 +342,10 @@ void featureFlagProxyRequestNeverCarriesDirectCredentials() throws Exception { final FakeFeaturesDiscovery discovery = new FakeFeaturesDiscovery(V4_EVP_PROXY_ENDPOINT); final BackendApiFactory factory = new BackendApiFactory( - Config.get(), sharedCommunicationObjects(discovery, agent.url("/")), true); + Config.get(), + sharedCommunicationObjects(discovery, agent.url("/")), + sdkHeaders(), + true); final BackendApi api = factory.createEvpProxyApi( Intake.EVENT_PLATFORM, @@ -355,6 +365,8 @@ void featureFlagProxyRequestNeverCarriesDirectCredentials() throws Exception { final RecordedRequest request = agent.takeRequest(); assertEquals("/evp_proxy/v4/api/v2/exposures", request.getPath()); assertEquals("event-platform-intake", request.getHeader("X-Datadog-EVP-Subdomain")); + assertEquals(JAVA_TRACING_LIBRARY, request.getHeader(ORIGIN_HEADER)); + assertEquals("test-version", request.getHeader(ORIGIN_VERSION_HEADER)); assertNull(request.getHeader("DD-API-KEY")); assertEquals(1, agent.getRequestCount()); } finally { @@ -363,7 +375,45 @@ void featureFlagProxyRequestNeverCarriesDirectCredentials() throws Exception { } @Test - void featureFlagDirectRequestCarriesCredentialsExactlyOnce() throws Exception { + void directIntakeSendsConfiguredRequestHeaders() throws Exception { + final MockWebServer intake = new MockWebServer(); + intake.enqueue(new MockResponse().setResponseCode(200).setBody("{}")); + intake.start(); + final SharedCommunicationObjects sco = + sharedCommunicationObjects(new FakeFeaturesDiscovery(null), null); + try { + final Properties properties = new Properties(); + properties.setProperty(API_KEY, "api-key"); + properties.setProperty( + CIVISIBILITY_AGENTLESS_URL, intake.url("/").toString().replaceAll("/$", "")); + final Config config = Config.get(properties); + final BackendApiFactory factory = + new BackendApiFactory( + config, sco, singletonMap(ORIGIN_HEADER, JAVA_TRACING_LIBRARY), false); + + // followRedirects=false mirrors the feature-flagging caller, so this also covers the + // interaction between the redirect-scoped client and the header interceptor. + final BackendApi api = factory.createDirectIntakeApi(Intake.API, false, false); + + assertNotNull(api); + api.post( + "flagevaluation", + RequestBody.create(JSON, "{}".getBytes(StandardCharsets.UTF_8)), + stream -> null, + null, + false); + + final RecordedRequest request = intake.takeRequest(); + assertEquals(JAVA_TRACING_LIBRARY, request.getHeader(ORIGIN_HEADER)); + assertEquals("api-key", request.getHeader("DD-API-KEY")); + } finally { + sco.getIntakeHttpClient().connectionPool().evictAll(); + intake.shutdown(); + } + } + + @Test + void featureFlagDirectRequestCarriesCredentialsAndIdentityExactlyOnce() throws Exception { final MockWebServer intake = new MockWebServer(); intake.enqueue(new MockResponse().setResponseCode(500).setBody("failed")); intake.enqueue(new MockResponse().setResponseCode(200).setBody("{}")); @@ -371,7 +421,10 @@ void featureFlagDirectRequestCarriesCredentialsExactlyOnce() throws Exception { final OkHttpClient sharedClient = new OkHttpClient.Builder().build(); final BackendApiFactory factory = new BackendApiFactory( - Config.get(), sharedCommunicationObjects(new FakeFeaturesDiscovery(null), null), true); + Config.get(), + sharedCommunicationObjects(new FakeFeaturesDiscovery(null), null), + sdkHeaders(), + true); final OkHttpClient directClient = factory.configureHttpClient(sharedClient); try { final IntakeApi api = @@ -396,6 +449,8 @@ void featureFlagDirectRequestCarriesCredentialsExactlyOnce() throws Exception { final RecordedRequest request = intake.takeRequest(); assertEquals("/api/v2/flagevaluation", request.getPath()); assertEquals("api-key", request.getHeader("DD-API-KEY")); + assertEquals("dd-trace-java", request.getHeader("DD-EVP-ORIGIN")); + assertEquals("test-version", request.getHeader("DD-EVP-ORIGIN-VERSION")); assertNull(request.getHeader("X-Datadog-EVP-Subdomain")); assertEquals(1, intake.getRequestCount()); assertEquals(false, directClient.retryOnConnectionFailure()); @@ -523,6 +578,13 @@ private static SharedCommunicationObjects sharedCommunicationObjects( return sco; } + private static Map sdkHeaders() { + final Map headers = new HashMap<>(2); + headers.put("DD-EVP-ORIGIN", "dd-trace-java"); + headers.put("DD-EVP-ORIGIN-VERSION", "test-version"); + return headers; + } + private static final class TestSharedCommunicationObjects extends SharedCommunicationObjects { private final DDAgentFeaturesDiscovery discovery; diff --git a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagBackendApiFactory.java b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagBackendApiFactory.java index 40c34395ab8..d4f3c8c499a 100644 --- a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagBackendApiFactory.java +++ b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagBackendApiFactory.java @@ -1,16 +1,23 @@ package com.datadog.featureflag; +import static datadog.communication.EvpProxy.JAVA_TRACING_LIBRARY; +import static datadog.communication.EvpProxy.ORIGIN_HEADER; +import static datadog.communication.EvpProxy.ORIGIN_VERSION_HEADER; import static datadog.communication.ddagent.DDAgentFeaturesDiscovery.V2_EVP_PROXY_ENDPOINT; import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.CONFIGURATION_SOURCE_AGENTLESS; -import static java.util.Collections.emptyList; +import static java.util.Arrays.asList; +import static java.util.Collections.unmodifiableMap; import datadog.communication.BackendApi; import datadog.communication.BackendApiFactory; import datadog.communication.ddagent.SharedCommunicationObjects; +import datadog.communication.ddagent.TracerVersion; import datadog.communication.http.HttpRetryPolicy; import datadog.trace.api.Config; import datadog.trace.api.intake.Intake; import java.net.ConnectException; +import java.util.HashMap; +import java.util.Map; import javax.annotation.Nullable; import okhttp3.Response; import org.slf4j.Logger; @@ -20,6 +27,7 @@ final class FeatureFlagBackendApiFactory { private static final Logger LOGGER = LoggerFactory.getLogger(FeatureFlagBackendApiFactory.class); + private static final Map REQUEST_HEADERS = requestHeaders(); /** Retries connection refusal only; automatic retries and redirects remain disabled. */ static final HttpRetryPolicy.Factory PRE_SEND_RETRY = @@ -85,7 +93,7 @@ private static BackendApiFactory initializedBackendFactory( // cached HTTP client uses configured timeouts even when Feature Flagging starts first. sharedCommunicationObjects.createRemaining(config); } - return new BackendApiFactory(config, sharedCommunicationObjects, true); + return new BackendApiFactory(config, sharedCommunicationObjects, REQUEST_HEADERS, true); } FeatureFlagBackendApiFactory( @@ -139,14 +147,22 @@ BackendApi create() { @Nullable private String discoverProxyEndpoint(final boolean forceDiscovery) { try { - // Identity-forwarding requirements belong with SDK identity emission in the identity PR. - return backendApiFactory.discoverEvpProxyEndpoint(forceDiscovery, emptyList()); + // A local route must preserve the identity headers that this sender emits on every batch. + return backendApiFactory.discoverEvpProxyEndpoint( + forceDiscovery, asList(ORIGIN_HEADER, ORIGIN_VERSION_HEADER)); } catch (final RuntimeException exception) { LOGGER.debug("Could not discover the local Feature Flagging route", exception); return null; } } + private static Map requestHeaders() { + final Map headers = new HashMap<>(2); + headers.put(ORIGIN_HEADER, JAVA_TRACING_LIBRARY); + headers.put(ORIGIN_VERSION_HEADER, TracerVersion.TRACER_VERSION); + return unmodifiableMap(headers); + } + private boolean hasDirectCredentials() { final String apiKey = config.getApiKey(); return apiKey != null && !apiKey.isEmpty(); diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/ExposureWriterTests.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/ExposureWriterTests.java index ca6d5256268..ca12d057578 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/ExposureWriterTests.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/ExposureWriterTests.java @@ -1,8 +1,10 @@ package com.datadog.featureflag; +import static datadog.communication.EvpProxy.ORIGIN_HEADER; +import static datadog.communication.EvpProxy.ORIGIN_VERSION_HEADER; import static datadog.communication.ddagent.DDAgentFeaturesDiscovery.V4_EVP_PROXY_ENDPOINT; import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.CONFIGURATION_SOURCE_AGENTLESS; -import static java.util.Collections.emptyList; +import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; import static java.util.concurrent.TimeUnit.MILLISECONDS; @@ -10,6 +12,7 @@ import static java.util.concurrent.TimeUnit.SECONDS; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -32,6 +35,7 @@ import datadog.communication.IntakeApi; import datadog.communication.ddagent.DDAgentFeaturesDiscovery; import datadog.communication.ddagent.SharedCommunicationObjects; +import datadog.communication.ddagent.TracerVersion; import datadog.communication.http.HttpRetryPolicy; import datadog.trace.agent.test.server.http.JavaTestHttpServer; import datadog.trace.agent.test.server.http.JavaTestHttpServer.HandlerApi; @@ -164,6 +168,32 @@ void testExposureEventWrites(String service, String env, String version) throws } } + @Test + void testExposureRequestsCarrySdkIdentity() throws Exception { + // Exercise the production writer and factory on the fixed Remote Config EVP v2 route. + Config config = mockConfig("identity-service"); + try (ExposureWriterImpl writer = + new ExposureWriterImpl(1 << 4, 100, MILLISECONDS, sharedCommunicationObjects, config)) { + writer.init(); + writer.accept(buildExposure()); + + poll.eventually( + () -> { + assertNotNull(server.getLastRequest()); + assertEquals(EXPOSURES_ENDPOINT, server.getLastRequest().getPath()); + assertEquals("dd-trace-java", server.getLastRequest().getHeader("DD-EVP-ORIGIN")); + assertEquals( + TracerVersion.TRACER_VERSION, + server.getLastRequest().getHeader("DD-EVP-ORIGIN-VERSION")); + // The test resource must prevent a hardcoded loader fallback from passing unnoticed. + assertNotEquals("0.0.0", server.getLastRequest().getHeader("DD-EVP-ORIGIN-VERSION")); + assertNull(server.getLastRequest().getHeader("DD-API-KEY")); + }); + } finally { + sharedCommunicationObjects.agentHttpClient.connectionPool().evictAll(); + } + } + @Test void testAgentlessExposureEventWritesDirectlyWithApiKey() throws Exception { Config config = mockConfig("test-service"); @@ -566,7 +596,8 @@ void testAmbiguousExposureBatchIsNotRetriedOrReplayedDirectly() throws Exception final BackendApiFactory backendApiFactory = mock(BackendApiFactory.class); final BackendApi proxyApi = mock(BackendApi.class); final BackendApi directApi = mock(BackendApi.class); - when(backendApiFactory.discoverEvpProxyEndpoint(false, emptyList())) + when(backendApiFactory.discoverEvpProxyEndpoint( + false, asList(ORIGIN_HEADER, ORIGIN_VERSION_HEADER))) .thenReturn(V4_EVP_PROXY_ENDPOINT); when(backendApiFactory.createEvpProxyApiForEndpoint( Intake.EVENT_PLATFORM, @@ -636,7 +667,8 @@ void idleUnavailableWriterOnlyProbesForANewBatchAndStillCloses() throws Exceptio final AtomicLong clock = new AtomicLong(); final BackendApiFactory backend = mock(BackendApiFactory.class); final BackendApi proxy = mock(BackendApi.class); - when(backend.discoverEvpProxyEndpoint(true, emptyList())).thenReturn(V4_EVP_PROXY_ENDPOINT); + when(backend.discoverEvpProxyEndpoint(true, asList(ORIGIN_HEADER, ORIGIN_VERSION_HEADER))) + .thenReturn(V4_EVP_PROXY_ENDPOINT); when(backend.createEvpProxyApiForEndpoint( Intake.EVENT_PLATFORM, true, @@ -653,14 +685,18 @@ void idleUnavailableWriterOnlyProbesForANewBatchAndStillCloses() throws Exceptio new ExposureWriterImpl(16, 100, MILLISECONDS, factory, config); try { writer.init(); - poll.eventually(() -> verify(backend).discoverEvpProxyEndpoint(false, emptyList())); + poll.eventually( + () -> + verify(backend) + .discoverEvpProxyEndpoint(false, asList(ORIGIN_HEADER, ORIGIN_VERSION_HEADER))); clock.set(10); MILLISECONDS.sleep(300); - verify(backend, never()).discoverEvpProxyEndpoint(true, emptyList()); + verify(backend, never()) + .discoverEvpProxyEndpoint(true, asList(ORIGIN_HEADER, ORIGIN_VERSION_HEADER)); verify(proxy, never()).post(any(), any(), any(), any(), eq(false)); writer.accept(buildExposure()); poll.eventually(() -> verify(proxy).post(eq("exposures"), any(), any(), any(), eq(false))); - verify(backend).discoverEvpProxyEndpoint(true, emptyList()); + verify(backend).discoverEvpProxyEndpoint(true, asList(ORIGIN_HEADER, ORIGIN_VERSION_HEADER)); } finally { writer.close(); } diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagBackendApiFactoryTest.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagBackendApiFactoryTest.java index fd020562d2d..2121d05846e 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagBackendApiFactoryTest.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagBackendApiFactoryTest.java @@ -2,10 +2,12 @@ import static com.datadog.featureflag.FeatureFlagEventType.EXPOSURE; import static com.datadog.featureflag.FeatureFlagEventType.FLAG_EVALUATION; +import static datadog.communication.EvpProxy.ORIGIN_HEADER; +import static datadog.communication.EvpProxy.ORIGIN_VERSION_HEADER; import static datadog.communication.ddagent.DDAgentFeaturesDiscovery.V2_EVP_PROXY_ENDPOINT; import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.CONFIGURATION_SOURCE_AGENTLESS; import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.CONFIGURATION_SOURCE_REMOTE_CONFIG; -import static java.util.Collections.emptyList; +import static java.util.Arrays.asList; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertSame; import static org.mockito.Mockito.mock; @@ -62,7 +64,8 @@ void remoteConfigUsesFixedV2WithoutDiscoveryOrDirectIntake() { new FeatureFlagBackendApiFactory(config, backendApiFactory, FLAG_EVALUATION).create(); assertSame(proxyApi, selected); - verify(backendApiFactory, never()).discoverEvpProxyEndpoint(false, emptyList()); + verify(backendApiFactory, never()) + .discoverEvpProxyEndpoint(false, asList(ORIGIN_HEADER, ORIGIN_VERSION_HEADER)); verify(backendApiFactory, never()).createDirectIntakeApi(Intake.EVENT_PLATFORM, false, false); } @@ -70,7 +73,8 @@ void remoteConfigUsesFixedV2WithoutDiscoveryOrDirectIntake() { void agentlessPrefersLocalRouteWithDirectFallbackReady() { final Config config = config(CONFIGURATION_SOURCE_AGENTLESS, "api-key"); final BackendApiFactory backendApiFactory = mock(BackendApiFactory.class); - when(backendApiFactory.discoverEvpProxyEndpoint(false, emptyList())) + when(backendApiFactory.discoverEvpProxyEndpoint( + false, asList(ORIGIN_HEADER, ORIGIN_VERSION_HEADER))) .thenReturn(V2_EVP_PROXY_ENDPOINT); when(backendApiFactory.createDirectIntakeApi(Intake.EVENT_PLATFORM, false, false)) .thenReturn(mock(BackendApi.class)); @@ -79,7 +83,8 @@ void agentlessPrefersLocalRouteWithDirectFallbackReady() { new FeatureFlagBackendApiFactory(config, backendApiFactory, FLAG_EVALUATION).create(); assertInstanceOf(AgentlessFeatureFlagBackendApi.class, selected); - verify(backendApiFactory).discoverEvpProxyEndpoint(false, emptyList()); + verify(backendApiFactory) + .discoverEvpProxyEndpoint(false, asList(ORIGIN_HEADER, ORIGIN_VERSION_HEADER)); verify(backendApiFactory).createDirectIntakeApi(Intake.EVENT_PLATFORM, false, false); } @@ -112,7 +117,8 @@ void agentlessKeepsWriterAliveWhileEveryRouteIsUnavailable() { void agentlessKeepsCompatibleLocalRouteWhenDirectUrlIsInvalid() { final Config config = config(CONFIGURATION_SOURCE_AGENTLESS, "api-key"); final BackendApiFactory backendApiFactory = mock(BackendApiFactory.class); - when(backendApiFactory.discoverEvpProxyEndpoint(false, emptyList())) + when(backendApiFactory.discoverEvpProxyEndpoint( + false, asList(ORIGIN_HEADER, ORIGIN_VERSION_HEADER))) .thenReturn(V2_EVP_PROXY_ENDPOINT); when(backendApiFactory.createDirectIntakeApi(Intake.EVENT_PLATFORM, false, false)) .thenThrow(new IllegalArgumentException("invalid URL")); diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagTransportRegressionTest.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagTransportRegressionTest.java index 1b19375ea44..81d91f63701 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagTransportRegressionTest.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagTransportRegressionTest.java @@ -11,6 +11,7 @@ import datadog.communication.BackendApi; import datadog.communication.ddagent.SharedCommunicationObjects; +import datadog.communication.ddagent.TracerVersion; import datadog.trace.api.Config; import datadog.trace.api.ProtocolVersion; import datadog.trace.test.junit.utils.config.WithConfig; @@ -21,6 +22,7 @@ import java.net.SocketTimeoutException; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeUnit; import okhttp3.HttpUrl; import okhttp3.MediaType; import okhttp3.OkHttpClient; @@ -33,7 +35,6 @@ import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import org.tabletest.junit.TableTest; @@ -137,35 +138,55 @@ void remoteConfigRetriesOnlyProvenPreSendFailures( } } - @Test - void agentlessWithoutIdentityKeepsAnOlderAgentUsableWithoutDirectCredentials() throws Exception { - final List requests = new ArrayList<>(); - final SharedCommunicationObjects sco = new SharedCommunicationObjects(); - sco.agentUrl = HttpUrl.get("http://localhost:8126/"); - sco.agentHttpClient = - new OkHttpClient.Builder() - .addInterceptor( - chain -> { - final Request request = chain.request(); - requests.add(request); - return response( - request, - 200, - request.url().encodedPath().equals("/info") - ? "{\"endpoints\":[\"v0.5/traces\",\"evp_proxy/v4/\"],\"evp_proxy_allowed_headers\":[]}" - : "{}"); - }) - .build(); - - final BackendApi api = - new FeatureFlagBackendApiFactory(config("agentless"), sco, EXPOSURE).create(); - post(api); - - assertEquals(2, requests.size()); - assertEquals("/info", requests.get(0).url().encodedPath()); - assertEquals("/evp_proxy/v4/api/v2/exposures", requests.get(1).url().encodedPath()); - assertNull(requests.get(1).header("DD-API-KEY")); - assertNull(requests.get(1).header("DD-EVP-ORIGIN")); + @TableTest({ + "Capabilities | Can Deliver", + "none | false ", + "origin | false ", + "version | false ", + "both | true " + }) + void agentlessIdentityRequiresBothForwardingCapabilities(String capabilities, boolean canDeliver) + throws Exception { + final String headers = + capabilities.equals("both") + ? "\"dd-evp-origin\",\"DD-EVP-ORIGIN-VERSION\"" + : capabilities.equals("origin") + ? "\"DD-EVP-ORIGIN\"" + : capabilities.equals("version") ? "\"DD-EVP-ORIGIN-VERSION\"" : ""; + try (MockWebServer server = new MockWebServer()) { + server.start(); + server.enqueue( + new MockResponse() + .setBody( + "{\"endpoints\":[\"v0.5/traces\",\"evp_proxy/v4/\"],\"evp_proxy_allowed_headers\":[" + + headers + + "]}")); + server.enqueue(new MockResponse().setResponseCode(202).setBody("{}")); + final SharedCommunicationObjects sco = new SharedCommunicationObjects(); + sco.agentUrl = server.url("/"); + sco.agentHttpClient = new OkHttpClient(); + try { + final BackendApi api = + new FeatureFlagBackendApiFactory(config("agentless"), sco, EXPOSURE).create(); + if (canDeliver) { + post(api); + } else { + assertThrows(IOException.class, () -> post(api)); + } + assertEquals(canDeliver ? 2 : 1, server.getRequestCount()); + assertEquals("/info", server.takeRequest(1, TimeUnit.SECONDS).getPath()); + if (canDeliver) { + final RecordedRequest event = server.takeRequest(1, TimeUnit.SECONDS); + assertEquals("/evp_proxy/v4/api/v2/exposures", event.getPath()); + assertNull(event.getHeader("DD-API-KEY")); + assertEquals("dd-trace-java", event.getHeader("DD-EVP-ORIGIN")); + assertEquals(TracerVersion.TRACER_VERSION, event.getHeader("DD-EVP-ORIGIN-VERSION")); + } + } finally { + sco.agentHttpClient.connectionPool().evictAll(); + sco.agentHttpClient.dispatcher().executorService().shutdownNow(); + } + } } private static Config config(String source) { diff --git a/products/feature-flagging/feature-flagging-lib/src/test/resources/dd-java-agent.version b/products/feature-flagging/feature-flagging-lib/src/test/resources/dd-java-agent.version new file mode 100644 index 00000000000..82c7ff5bb58 --- /dev/null +++ b/products/feature-flagging/feature-flagging-lib/src/test/resources/dd-java-agent.version @@ -0,0 +1 @@ +TEST-SNAPSHOT~identity-test