diff --git a/dd-smoke-tests/dynamic-config/build.gradle b/dd-smoke-tests/dynamic-config/build.gradle index 2efcbc9e971..a74375379bb 100644 --- a/dd-smoke-tests/dynamic-config/build.gradle +++ b/dd-smoke-tests/dynamic-config/build.gradle @@ -14,6 +14,9 @@ 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 { diff --git a/dd-smoke-tests/dynamic-config/src/test/groovy/datadog/smoketest/AppSecActivationSmokeTest.groovy b/dd-smoke-tests/dynamic-config/src/test/groovy/datadog/smoketest/AppSecActivationSmokeTest.groovy deleted file mode 100644 index 513cf008c87..00000000000 --- a/dd-smoke-tests/dynamic-config/src/test/groovy/datadog/smoketest/AppSecActivationSmokeTest.groovy +++ /dev/null @@ -1,89 +0,0 @@ -package datadog.smoketest - -import datadog.environment.JavaVirtualMachine -import datadog.remoteconfig.Capabilities -import datadog.remoteconfig.Product -import datadog.smoketest.dynamicconfig.AppSecApplication -import datadog.trace.test.util.Flaky - -class AppSecActivationSmokeTest extends AbstractSmokeTest { - - @Override - ProcessBuilder createProcessBuilder() { - def command = [javaPath()] - command += defaultJavaProperties.toList() - command += [ - '-Ddd.remote_config.enabled=true', - "-Ddd.remote_config.url=http://localhost:${server.address.port}/v0.7/config".toString(), - '-Ddd.remote_config.poll_interval.seconds=1', - '-Ddd.profiling.enabled=false', - '-cp', - System.getProperty('datadog.smoketest.shadowJar.path'), - AppSecApplication.name - ] - - final processBuilder = new ProcessBuilder(command) - processBuilder.directory(new File(buildDirectory)) - } - - @Flaky(value = "Telemetry product change event flakes in oracle8", condition = () ->JavaVirtualMachine.isOracleJDK8()) - void 'test activation via RC workflow'() { - given: - final asmRuleProducts = [Product.ASM, Product.ASM_DD, Product.ASM_DATA] - - when: 'appsec is enabled but inactive' - final request = waitForRcClientRequest { - req -> - decodeProducts(req).find { - asmRuleProducts.contains(it) - } == null - } - final capabilities = decodeCapabilities(request) - - then: 'only ASM_ACTIVATION capability should be reported' - assert hasCapability(capabilities, Capabilities.CAPABILITY_ASM_ACTIVATION) - assert !hasCapability(capabilities, Capabilities.CAPABILITY_ASM_CUSTOM_RULES) - - when: 'appsec is enabled via RC' - setRemoteConfig('datadog/2/ASM_FEATURES/asm_features_activation/config', '{"asm":{"enabled":true}}') - - then: 'we should receive a product change for appsec' - waitForTelemetryFlat { - final configurations = (List>) it?.payload?.configuration ?: [] - final enabledConfig = configurations.find { - it.name == 'DD_APPSEC_ENABLED' - } - if (!enabledConfig) { - return false - } - return enabledConfig.value == 'true' && enabledConfig .origin == 'remote_config' - } - - and: 'we should have set the capabilities for ASM rules and data' - final newRequest = waitForRcClientRequest { - req -> - decodeProducts(req).containsAll(asmRuleProducts) - } - final newCapabilities = decodeCapabilities(newRequest) - assert hasCapability(newCapabilities, Capabilities.CAPABILITY_ASM_CUSTOM_RULES) - } - - private static Set decodeProducts(final Map request) { - return request.client.products.collect { - Product.valueOf(it) - } - } - - private static long decodeCapabilities(final Map request) { - final clientCapabilities = request.client.capabilities as byte[] - long capabilities = 0l - for (int i = 0; i < clientCapabilities.length; i++) { - capabilities |= (clientCapabilities[i] & 0xFFL) << ((clientCapabilities.length - i - 1) * 8) - } - return capabilities - } - - private static boolean hasCapability(final long capabilities, final long test) { - return (capabilities & test) > 0 - } -} diff --git a/dd-smoke-tests/dynamic-config/src/test/groovy/datadog/smoketest/DynamicServiceMappingSmokeTest.groovy b/dd-smoke-tests/dynamic-config/src/test/groovy/datadog/smoketest/DynamicServiceMappingSmokeTest.groovy deleted file mode 100644 index b2898fe7e63..00000000000 --- a/dd-smoke-tests/dynamic-config/src/test/groovy/datadog/smoketest/DynamicServiceMappingSmokeTest.groovy +++ /dev/null @@ -1,45 +0,0 @@ -package datadog.smoketest - -import datadog.smoketest.dynamicconfig.ServiceMappingApplication - -import static java.util.concurrent.TimeUnit.SECONDS - -class DynamicServiceMappingSmokeTest extends AbstractSmokeTest { - // Estimate for the amount of time instrumentation, plus request, plus some extra - public static final int TIMEOUT_SECS = 30 - - @Override - ProcessBuilder createProcessBuilder() { - List command = new ArrayList<>() - command.add(javaPath()) - command.addAll(defaultJavaProperties) - command.add("-Ddd.remote_config.enabled=true") - command.add("-Ddd.remote_config.url=http://localhost:${server.address.port}/v0.7/config".toString()) - command.add("-Ddd.remote_config.poll_interval.seconds=1") - command.addAll((String[]) ["-cp", System.getProperty("datadog.smoketest.shadowJar.path")]) - command.add(ServiceMappingApplication.name) - - ProcessBuilder processBuilder = new ProcessBuilder(command) - processBuilder.directory(new File(buildDirectory)) - } - - def "Updated service mapping observed"() { - when: - def newConfig = """ - { - "lib_config": { - "tracing_service_mapping": [{ - "from_key": "${ServiceMappingApplication.ORIGINAL_SERVICE_NAME}", - "to_name": "${ServiceMappingApplication.MAPPED_SERVICE_NAME}" - }] - } - } - """ as String - - setRemoteConfig("datadog/2/APM_TRACING/config_overrides/config", newConfig) - - then: - assert testedProcess.waitFor(TIMEOUT_SECS, SECONDS) - assert testedProcess.exitValue() == 0 - } -} diff --git a/dd-smoke-tests/dynamic-config/src/test/java/datadog/smoketest/AppSecActivationSmokeTest.java b/dd-smoke-tests/dynamic-config/src/test/java/datadog/smoketest/AppSecActivationSmokeTest.java new file mode 100644 index 00000000000..19e30e0b16f --- /dev/null +++ b/dd-smoke-tests/dynamic-config/src/test/java/datadog/smoketest/AppSecActivationSmokeTest.java @@ -0,0 +1,129 @@ +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 datadog.smoketest.dynamicconfig.AppSecApplication.TIMEOUT_IN_SECONDS; +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.AgentBackend; +import datadog.smoketest.backend.RemoteConfig; +import datadog.smoketest.backend.Telemetry; +import datadog.smoketest.backend.TestAgentBackend; +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. 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. + * + *

{@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. + */ +class AppSecActivationSmokeTest { + + private static final EnumSet 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. + static final AgentBackend agent = AgentBackend.testAgent(); + + @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(); + Telemetry telemetry = agent.telemetry(); + + // 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 beforeActivation = + remoteConfig.waitForRequest( + request -> disjoint(decodeProducts(request), ASM_RULE_PRODUCTS), TIMEOUT_IN_SECONDS); + 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. + telemetry.waitForFlat( + AppSecActivationSmokeTest::appsecEnabledFromRemoteConfig, TIMEOUT_IN_SECONDS); + + // Now active: the tracer subscribes to the ASM rule products and advertises ASM_CUSTOM_RULES. + Map afterActivation = + remoteConfig.waitForRequest( + request -> decodeProducts(request).containsAll(ASM_RULE_PRODUCTS), TIMEOUT_IN_SECONDS); + 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 event) { + Object payload = event.get("payload"); + if (!(payload instanceof Map)) { + return false; + } + Object configuration = ((Map) payload).get("configuration"); + if (!(configuration instanceof List)) { + return false; + } + for (Object entry : (List) configuration) { + if (entry instanceof Map) { + Map config = (Map) entry; + if ("DD_APPSEC_ENABLED".equals(config.get("name")) + && "true".equals(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 decodeProducts(Map request) { + Set 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; + } +} diff --git a/dd-smoke-tests/dynamic-config/src/test/java/datadog/smoketest/DynamicServiceMappingSmokeTest.java b/dd-smoke-tests/dynamic-config/src/test/java/datadog/smoketest/DynamicServiceMappingSmokeTest.java new file mode 100644 index 00000000000..73b77b9ec09 --- /dev/null +++ b/dd-smoke-tests/dynamic-config/src/test/java/datadog/smoketest/DynamicServiceMappingSmokeTest.java @@ -0,0 +1,56 @@ +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.AgentBackend; +import datadog.smoketest.backend.TestAgentBackend; +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} -> {@value + * ServiceMappingApplication#MAPPED_SERVICE_NAME}) from its {@code /v0.7/config} poll. Ported from + * the Groovy {@code DynamicServiceMappingSmokeTest}. + * + *

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. + */ +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 = AgentBackend.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); + } +}