Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ public SpanMatcher childOfPrevious() {
* @return The current {@link SpanMatcher} instance with the child-of constraint applied.
*/
public SpanMatcher childOfIndex(int parentSpanIndex) {
if (parentSpanIndex < 0) {
throw new AssertionFailedError("index must be >= 0");
}
this.parentIdMatcher = null;
this.parentSpanIndex = parentSpanIndex;
return this;
Expand Down Expand Up @@ -451,6 +454,7 @@ private void assertSpanLinks(List<AgentSpanLink> links) {
.expected(expectedLinkCount)
.actual(linkCount)
.buildAndThrow();
return;
}
for (int i = 0; i < expectedLinkCount; i++) {
SpanLinkMatcher linkMatcher = this.linkMatchers[i];
Expand Down
2 changes: 1 addition & 1 deletion dd-smoke-tests/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ description = 'dd-smoke-tests'

dependencies {
api libs.okhttp
api libs.testcontainers
api project(':dd-java-agent:testing')
api project(':utils:test-agent-utils:decoder')

compileOnly(libs.junit.jupiter)

compileOnly(libs.bundles.groovy)
compileOnly(libs.bundles.spock)
}
Expand Down
4 changes: 4 additions & 0 deletions dd-smoke-tests/dynamic-config/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@ dependencies {

testImplementation project(':dd-smoke-tests')
testImplementation project(':utils:test-utils')
testImplementation project(':components:environment')
testImplementation project(':remote-config:remote-config-api')
testImplementation libs.testcontainers
}

tasks.withType(Test).configureEach {
usesService(testcontainersLimit)
dependsOn "shadowJar"

def shadowJarTask = tasks.named('shadowJar', ShadowJar)
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package datadog.smoketest;

import static datadog.environment.JavaVirtualMachine.isOracleJDK8;
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_ACTIVATION;
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_CUSTOM_RULES;
import static datadog.remoteconfig.Product.ASM;
import static datadog.remoteconfig.Product.ASM_DATA;
import static datadog.remoteconfig.Product.ASM_DD;
import static java.util.Collections.disjoint;
import static java.util.EnumSet.of;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeFalse;

import datadog.remoteconfig.Product;
import datadog.smoketest.backend.EnabledIfDockerAvailable;
import datadog.smoketest.backend.RemoteConfig;
import datadog.smoketest.backend.TestAgentBackend;
import datadog.smoketest.backend.TraceBackend;
import datadog.smoketest.dynamicconfig.AppSecApplication;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

/**
* Verifies AppSec activation via Remote Configuration, ported from the Groovy {@code
* AppSecActivationSmokeTest}. With AppSec enabled but inactive, the tracer advertises only the
* ASM_ACTIVATION capability; once a config activating AppSec is pushed, the tracer reports the
* change via telemetry (DD_APPSEC_ENABLED, origin {@code remote_config}) and then subscribes to the
* ASM rule products (ASM, ASM_DD, ASM_DATA), advertising ASM_CUSTOM_RULES.
*
* <p>{@link AppSecApplication} just stays alive briefly while its tracer polls Remote Config, so
* the whole activation workflow is asserted from the poll requests and telemetry captured by the
* {@link TestAgentBackend}. Telemetry is central here, so the base's telemetry support is left
* enabled: it flushes the app on a fast heartbeat (so the config-change event arrives within the
* app's short lifetime) and additionally asserts telemetry is flowing.
*/
@EnabledIfDockerAvailable
class AppSecActivationSmokeTest {

private static final EnumSet<Product> ASM_RULE_PRODUCTS = of(ASM, ASM_DD, ASM_DATA);

// Inline backend owned by the app; held as a field so the test can push and read remote-config.
private static final TestAgentBackend agent = TraceBackend.testAgentBuilder().build();

@RegisterExtension
static final SmokeCliApp app =
SmokeCliApp.named("appsec-activation")
.mainClass(AppSecApplication.class.getName())
.classpath(System.getProperty("datadog.smoketest.shadowJar.path"))
.jvmArgs("-Ddd.remote_config.enabled=true", "-Ddd.remote_config.poll_interval.seconds=1")
.backend(agent)
.build();

@Test
void activatesAppSecViaRemoteConfig() {
assumeFalse(isOracleJDK8(), "Telemetry product-change event flakes on Oracle JDK 8");

RemoteConfig remoteConfig = agent.remoteConfig();

// AppSec is enabled but inactive: a poll that has not subscribed to any ASM rule product yet
// advertises the ASM_ACTIVATION capability, but not ASM_CUSTOM_RULES.
Map<String, Object> beforeActivation =
remoteConfig.waitForRequest(
request -> disjoint(decodeProducts(request), ASM_RULE_PRODUCTS));
long capabilities = RemoteConfig.capabilities(beforeActivation);
assertTrue(hasCapability(capabilities, CAPABILITY_ASM_ACTIVATION), "ASM_ACTIVATION advertised");
assertFalse(
hasCapability(capabilities, CAPABILITY_ASM_CUSTOM_RULES),
"ASM_CUSTOM_RULES not advertised while inactive");

// Activate AppSec via Remote Config.
remoteConfig.setConfig(
"datadog/2/ASM_FEATURES/asm_features_activation/config", "{\"asm\":{\"enabled\":true}}");

// The tracer reports the applied change via a telemetry configuration event.
agent.telemetry().waitForFlat(AppSecActivationSmokeTest::appsecEnabledFromRemoteConfig);

// Now active: the tracer subscribes to the ASM rule products and advertises ASM_CUSTOM_RULES.
Map<String, Object> afterActivation =
remoteConfig.waitForRequest(
request -> decodeProducts(request).containsAll(ASM_RULE_PRODUCTS));
assertTrue(
hasCapability(RemoteConfig.capabilities(afterActivation), CAPABILITY_ASM_CUSTOM_RULES),
"ASM_CUSTOM_RULES advertised after activation");
}

// A flattened telemetry event whose payload records DD_APPSEC_ENABLED=true from remote config.
@SuppressWarnings("unchecked")
private static boolean appsecEnabledFromRemoteConfig(Map<String, Object> event) {
Object payload = event.get("payload");
if (!(payload instanceof Map)) {
return false;
}
Object configuration = ((Map<String, Object>) payload).get("configuration");
if (!(configuration instanceof List)) {
return false;
}
for (Object entry : (List<?>) configuration) {
if (entry instanceof Map) {
Map<String, Object> config = (Map<String, Object>) entry;
if ("DD_APPSEC_ENABLED".equals(config.get("name"))
&& "true".equals(String.valueOf(config.get("value")))
&& "remote_config".equals(config.get("origin"))) {
return true;
}
}
}
return false;
}

// The products a Remote Config poll subscribes to, decoded from the wire strings into typed
// Product values (the tracer serializes them from this same enum).
private static Set<Product> decodeProducts(Map<String, Object> request) {
Set<Product> products = EnumSet.noneOf(Product.class);
for (String name : RemoteConfig.products(request)) {
products.add(Product.valueOf(name));
}
return products;
}

private static boolean hasCapability(long capabilities, long capability) {
return (capabilities & capability) != 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package datadog.smoketest;

import static datadog.smoketest.dynamicconfig.ServiceMappingApplication.MAPPED_SERVICE_NAME;
import static datadog.smoketest.dynamicconfig.ServiceMappingApplication.ORIGINAL_SERVICE_NAME;
import static java.util.concurrent.TimeUnit.SECONDS;

import datadog.smoketest.backend.EnabledIfDockerAvailable;
import datadog.smoketest.backend.TestAgentBackend;
import datadog.smoketest.backend.TraceBackend;
import datadog.smoketest.dynamicconfig.ServiceMappingApplication;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

/**
* Verifies dynamic service mapping via Remote Configuration: pushes an {@code APM_TRACING}
* service-mapping config to the test agent, and the launched {@link ServiceMappingApplication}
* exits 0 once its tracer applies the mapping ({@value
* ServiceMappingApplication#ORIGINAL_SERVICE_NAME} -&gt; {@value
* ServiceMappingApplication#MAPPED_SERVICE_NAME}) from its {@code /v0.7/config} poll. Ported from
* the Groovy {@code DynamicServiceMappingSmokeTest}.
*
* <p>The tracer's Remote Config poller shares the agent HTTP client that carries the {@code
* X-Datadog-Test-Session-Token}, so the config pushed to this backend's session reaches this app's
* tracer. Telemetry is not the subject here, so the default telemetry check is skipped.
*/
@EnabledIfDockerAvailable
class DynamicServiceMappingSmokeTest {

// Inline backend owned by the app; held as a field so the test can push a remote-config payload.
private static final TestAgentBackend agent = TraceBackend.testAgentBuilder().build();

@RegisterExtension
static final SmokeCliApp app =
SmokeCliApp.named("dynamic-service-mapping")
.mainClass(ServiceMappingApplication.class.getName())
.classpath(System.getProperty("datadog.smoketest.shadowJar.path"))
.jvmArgs("-Ddd.remote_config.enabled=true", "-Ddd.remote_config.poll_interval.seconds=1")
.backend(agent)
.skipTelemetryCheck()
.build();

@Test
void updatedServiceMappingObserved() {
// Push a service-mapping override; the tracer picks it up on its next /v0.7/config poll and the
// app exits 0 once it observes the remapped service name (or exits 1 after its 10s timeout).
agent
.remoteConfig()
.setConfig(
"datadog/2/APM_TRACING/config_overrides/config",
"{\"lib_config\":{\"tracing_service_mapping\":[{"
+ "\"from_key\":\""
+ ORIGINAL_SERVICE_NAME
+ "\",\"to_name\":\""
+ MAPPED_SERVICE_NAME
+ "\"}]}}");
app.assertCompletesWithValue(30, SECONDS, 0);
}
}
Loading
Loading