Skip to content

Commit 01b6a17

Browse files
mhliddbric3
andcommitted
Adding Gradle Plugins for Config Inversion (#9565)
* adding supported-configurations.json file * adding gradle plugins for config inversion * hooking parsing json in config-utils * excluding json from shadowjar * restricting config inversion files from being duplicated in shadow jars * attempting to fix published_artifacts job * moving supported-configurations.json to metadata * updating gradle files * updating plugin to account for file being in metadata * PR comments * chore: Tweak comment * updating gradle to not exclude config inversion --------- Co-authored-by: Brice Dutheil <brice.dutheil@gmail.com>
1 parent d20ea1d commit 01b6a17

File tree

7 files changed

+342
-0
lines changed

7 files changed

+342
-0
lines changed

buildSrc/build.gradle.kts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ gradlePlugin {
3030
id = "tracer-version"
3131
implementationClass = "datadog.gradle.plugin.version.TracerVersionPlugin"
3232
}
33+
create("supported-config-generation") {
34+
id = "supported-config-generator"
35+
implementationClass = "datadog.gradle.plugin.config.SupportedConfigPlugin"
36+
}
37+
create("supported-config-linter") {
38+
id = "config-inversion-linter"
39+
implementationClass = "datadog.gradle.plugin.config.ConfigInversionLinter"
40+
}
3341
}
3442
}
3543

@@ -52,6 +60,11 @@ dependencies {
5260
implementation("com.google.guava", "guava", "20.0")
5361
implementation("org.ow2.asm", "asm", "9.8")
5462
implementation("org.ow2.asm", "asm-tree", "9.8")
63+
64+
implementation(platform("com.fasterxml.jackson:jackson-bom:2.17.2"))
65+
implementation("com.fasterxml.jackson.core:jackson-databind")
66+
implementation("com.fasterxml.jackson.core:jackson-annotations")
67+
implementation("com.fasterxml.jackson.core:jackson-core")
5568
}
5669

5770
tasks.compileKotlin {
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package datadog.gradle.plugin.config
2+
3+
import org.gradle.api.Plugin
4+
import org.gradle.api.Project
5+
import org.gradle.api.GradleException
6+
import org.gradle.api.tasks.SourceSet
7+
import org.gradle.api.tasks.SourceSetContainer
8+
import org.gradle.internal.impldep.kotlinx.metadata.impl.extensions.KmExtension
9+
import org.gradle.kotlin.dsl.accessors.runtime.externalModuleDependencyFor
10+
import org.gradle.kotlin.dsl.getByType
11+
import java.net.URLClassLoader
12+
import java.nio.file.Path
13+
14+
class ConfigInversionLinter : Plugin<Project> {
15+
override fun apply(target: Project) {
16+
val extension = target.extensions.create("supportedTracerConfigurations", SupportedTracerConfigurations::class.java)
17+
registerLogEnvVarUsages(target, extension)
18+
registerCheckEnvironmentVariablesUsage(target)
19+
}
20+
}
21+
22+
/** Registers `logEnvVarUsages` (scan for DD_/OTEL_ tokens and fail if unsupported). */
23+
private fun registerLogEnvVarUsages(target: Project, extension: SupportedTracerConfigurations) {
24+
val ownerPath = extension.configOwnerPath
25+
val generatedFile = extension.className
26+
27+
// token check that uses the generated class instead of JSON
28+
target.tasks.register("logEnvVarUsages") {
29+
group = "verification"
30+
description = "Scan Java files for DD_/OTEL_ tokens and fail if unsupported (using generated constants)"
31+
32+
val mainSourceSetOutput = ownerPath.map {
33+
target.project(it)
34+
.extensions.getByType<SourceSetContainer>()
35+
.named(SourceSet.MAIN_SOURCE_SET_NAME)
36+
.map { main -> main.output }
37+
}
38+
inputs.files(mainSourceSetOutput)
39+
40+
// inputs for incrementality (your own source files, not the owner’s)
41+
val javaFiles = target.fileTree(target.projectDir) {
42+
include("**/src/main/java/**/*.java")
43+
exclude("**/build/**", "**/dd-smoke-tests/**")
44+
}
45+
inputs.files(javaFiles)
46+
outputs.upToDateWhen { true }
47+
doLast {
48+
// 1) Build classloader from the owner project’s runtime classpath
49+
val urls = mainSourceSetOutput.get().get().files.map { it.toURI().toURL() }.toTypedArray()
50+
val supported: Set<String> = URLClassLoader(urls, javaClass.classLoader).use { cl ->
51+
// 2) Load the generated class + read static field
52+
val clazz = Class.forName(generatedFile.get(), true, cl)
53+
@Suppress("UNCHECKED_CAST")
54+
clazz.getField("SUPPORTED").get(null) as Set<String>
55+
}
56+
57+
// 3) Scan our sources and compare
58+
val repoRoot = target.projectDir.toPath()
59+
val tokenRegex = Regex("\"(?:DD_|OTEL_)[A-Za-z0-9_]+\"")
60+
61+
val violations = buildList {
62+
javaFiles.files.forEach { f ->
63+
val rel = repoRoot.relativize(f.toPath()).toString()
64+
var inBlock = false
65+
f.readLines().forEachIndexed { i, raw ->
66+
val trimmed = raw.trim()
67+
if (trimmed.startsWith("//")) return@forEachIndexed
68+
if (!inBlock && trimmed.contains("/*")) inBlock = true
69+
if (inBlock) {
70+
if (trimmed.contains("*/")) inBlock = false
71+
return@forEachIndexed
72+
}
73+
tokenRegex.findAll(raw).forEach { m ->
74+
val token = m.value.trim('"')
75+
if (token !in supported) add("$rel:${i + 1} -> Unsupported token'$token'")
76+
}
77+
}
78+
}
79+
}
80+
81+
if (violations.isNotEmpty()) {
82+
violations.forEach { target.logger.error(it) }
83+
throw GradleException("Unsupported DD_/OTEL_ tokens found! See errors above.")
84+
} else {
85+
target.logger.info("All DD_/OTEL_ tokens are supported.")
86+
}
87+
}
88+
}
89+
}
90+
91+
/** Registers `checkEnvironmentVariablesUsage` (forbid EnvironmentVariables.get(...)). */
92+
private fun registerCheckEnvironmentVariablesUsage(project: Project) {
93+
project.tasks.register("checkEnvironmentVariablesUsage") {
94+
group = "verification"
95+
description = "Scans src/main/java for direct usages of EnvironmentVariables.get(...)"
96+
97+
doLast {
98+
val repoRoot: Path = project.projectDir.toPath()
99+
val javaFiles = project.fileTree(project.projectDir) {
100+
include("**/src/main/java/**/*.java")
101+
exclude("**/build/**")
102+
exclude("internal-api/src/main/java/datadog/trace/api/ConfigHelper.java")
103+
exclude("dd-java-agent/agent-bootstrap/**")
104+
exclude("dd-java-agent/src/main/java/datadog/trace/bootstrap/BootstrapInitializationTelemetry.java")
105+
}
106+
107+
val pattern = Regex("""EnvironmentVariables\.get\s*\(""")
108+
val matches = buildList {
109+
javaFiles.forEach { f ->
110+
val relative = repoRoot.relativize(f.toPath())
111+
f.readLines().forEachIndexed { idx, line ->
112+
if (pattern.containsMatchIn(line)) {
113+
add("$relative:${idx + 1} -> ${line.trim()}")
114+
}
115+
}
116+
}
117+
}
118+
119+
if (matches.isNotEmpty()) {
120+
project.logger.lifecycle("\nFound forbidden usages of EnvironmentVariables.get(...):")
121+
matches.forEach { project.logger.lifecycle(it) }
122+
throw GradleException("Forbidden usage of EnvironmentVariables.get(...) found in Java files.")
123+
} else {
124+
project.logger.info("No forbidden EnvironmentVariables.get(...) usages found in src/main/java.")
125+
}
126+
}
127+
}
128+
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package datadog.gradle.plugin.config
2+
3+
import org.gradle.api.DefaultTask
4+
import org.gradle.api.model.ObjectFactory
5+
import org.gradle.api.tasks.Input
6+
import org.gradle.api.tasks.InputFile
7+
import org.gradle.api.tasks.OutputDirectory
8+
import org.gradle.api.tasks.TaskAction
9+
import com.fasterxml.jackson.core.type.TypeReference
10+
import com.fasterxml.jackson.databind.ObjectMapper
11+
import org.gradle.api.tasks.CacheableTask
12+
import org.gradle.api.tasks.PathSensitive
13+
import org.gradle.api.tasks.PathSensitivity
14+
import java.io.File
15+
import java.io.FileInputStream
16+
import java.io.PrintWriter
17+
import javax.inject.Inject
18+
19+
@CacheableTask
20+
abstract class ParseSupportedConfigurationsTask @Inject constructor(
21+
private val objects: ObjectFactory
22+
) : DefaultTask() {
23+
@InputFile
24+
@PathSensitive(PathSensitivity.NONE)
25+
val jsonFile = objects.fileProperty()
26+
27+
@get:OutputDirectory
28+
val destinationDirectory = objects.directoryProperty()
29+
30+
@Input
31+
val className = objects.property(String::class.java)
32+
33+
@TaskAction
34+
fun generate() {
35+
val input = jsonFile.get().asFile
36+
val outputDir = destinationDirectory.get().asFile
37+
val finalClassName = className.get()
38+
outputDir.mkdirs()
39+
40+
// Read JSON (directly from the file, not classpath)
41+
val mapper = ObjectMapper()
42+
val fileData: Map<String, Any?> = FileInputStream(input).use { inStream ->
43+
mapper.readValue(inStream, object : TypeReference<Map<String, Any?>>() {})
44+
}
45+
46+
@Suppress("UNCHECKED_CAST")
47+
val supported = fileData["supportedConfigurations"] as Map<String, List<String>>
48+
@Suppress("UNCHECKED_CAST")
49+
val aliases = fileData["aliases"] as Map<String, List<String>>
50+
@Suppress("UNCHECKED_CAST")
51+
val deprecated = (fileData["deprecations"] as? Map<String, String>) ?: emptyMap()
52+
53+
val aliasMapping = mutableMapOf<String, String>()
54+
for ((canonical, alist) in aliases) {
55+
for (alias in alist) aliasMapping[alias] = canonical
56+
}
57+
58+
// Build the output .java path from the fully-qualified class name
59+
val pkgName = finalClassName.substringBeforeLast('.', "")
60+
val pkgPath = pkgName.replace('.', File.separatorChar)
61+
val simpleName = finalClassName.substringAfterLast('.')
62+
val pkgDir = if (pkgPath.isEmpty()) outputDir else File(outputDir, pkgPath).also { it.mkdirs() }
63+
val generatedFile = File(pkgDir, "$simpleName.java").absolutePath
64+
65+
// Call your existing generator (same signature as in your Java code)
66+
generateJavaFile(
67+
generatedFile,
68+
simpleName,
69+
pkgName,
70+
supported.keys,
71+
aliases,
72+
aliasMapping,
73+
deprecated
74+
)
75+
}
76+
77+
private fun generateJavaFile(
78+
outputPath: String,
79+
className: String,
80+
packageName: String,
81+
supportedKeys: Set<String>,
82+
aliases: Map<String, List<String>>,
83+
aliasMapping: Map<String, String>,
84+
deprecated: Map<String, String>
85+
) {
86+
val outFile = File(outputPath)
87+
outFile.parentFile?.mkdirs()
88+
89+
PrintWriter(outFile).use { out ->
90+
// NOTE: adjust these if you want to match task's className
91+
out.println("package $packageName;")
92+
out.println()
93+
out.println("import java.util.*;")
94+
out.println()
95+
out.println("public final class $className {")
96+
out.println()
97+
out.println(" public static final Set<String> SUPPORTED;")
98+
out.println()
99+
out.println(" public static final Map<String, List<String>> ALIASES;")
100+
out.println()
101+
out.println(" public static final Map<String, String> ALIAS_MAPPING;")
102+
out.println()
103+
out.println(" public static final Map<String, String> DEPRECATED;")
104+
out.println()
105+
out.println(" static {")
106+
out.println()
107+
108+
// SUPPORTED
109+
out.print(" Set<String> supportedSet = new HashSet<>(Arrays.asList(")
110+
val supportedIter = supportedKeys.toSortedSet().iterator()
111+
while (supportedIter.hasNext()) {
112+
val key = supportedIter.next()
113+
out.print("\"${esc(key)}\"")
114+
if (supportedIter.hasNext()) out.print(", ")
115+
}
116+
out.println("));")
117+
out.println(" SUPPORTED = Collections.unmodifiableSet(supportedSet);")
118+
out.println()
119+
120+
// ALIASES
121+
out.println(" Map<String, List<String>> aliasesMap = new HashMap<>();")
122+
for ((canonical, list) in aliases.toSortedMap()) {
123+
out.printf(
124+
" aliasesMap.put(\"%s\", Collections.unmodifiableList(Arrays.asList(%s)));\n",
125+
esc(canonical),
126+
quoteList(list)
127+
)
128+
}
129+
out.println(" ALIASES = Collections.unmodifiableMap(aliasesMap);")
130+
out.println()
131+
132+
// ALIAS_MAPPING
133+
out.println(" Map<String, String> aliasMappingMap = new HashMap<>();")
134+
for ((alias, target) in aliasMapping.toSortedMap()) {
135+
out.printf(" aliasMappingMap.put(\"%s\", \"%s\");\n", esc(alias), esc(target))
136+
}
137+
out.println(" ALIAS_MAPPING = Collections.unmodifiableMap(aliasMappingMap);")
138+
out.println()
139+
140+
// DEPRECATED
141+
out.println(" Map<String, String> deprecatedMap = new HashMap<>();")
142+
for ((oldKey, note) in deprecated.toSortedMap()) {
143+
out.printf(" deprecatedMap.put(\"%s\", \"%s\");\n", esc(oldKey), esc(note))
144+
}
145+
out.println(" DEPRECATED = Collections.unmodifiableMap(deprecatedMap);")
146+
out.println()
147+
out.println(" }")
148+
out.println("}")
149+
}
150+
}
151+
152+
private fun quoteList(list: List<String>): String =
153+
list.joinToString(", ") { "\"${esc(it)}\"" }
154+
155+
private fun esc(s: String): String =
156+
s.replace("\\", "\\\\").replace("\"", "\\\"")
157+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package datadog.gradle.plugin.config
2+
3+
import org.gradle.api.Plugin
4+
import org.gradle.api.Project
5+
import org.gradle.api.tasks.SourceSet
6+
import org.gradle.api.tasks.SourceSetContainer
7+
8+
class SupportedConfigPlugin : Plugin<Project> {
9+
override fun apply(targetProject: Project) {
10+
val extension = targetProject.extensions.create("supportedTracerConfigurations", SupportedTracerConfigurations::class.java)
11+
generateSupportedConfigurations(targetProject, extension)
12+
}
13+
14+
private fun generateSupportedConfigurations(targetProject: Project, extension: SupportedTracerConfigurations) {
15+
val generateTask =
16+
targetProject.tasks.register("generateSupportedConfigurations", ParseSupportedConfigurationsTask::class.java) {
17+
jsonFile.set(extension.jsonFile)
18+
destinationDirectory.set(extension.destinationDirectory)
19+
className.set(extension.className)
20+
}
21+
22+
val sourceset = targetProject.extensions.getByType(SourceSetContainer::class.java).named(SourceSet.MAIN_SOURCE_SET_NAME)
23+
sourceset.configure {
24+
java.srcDir(generateTask)
25+
}
26+
}
27+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package datadog.gradle.plugin.config
2+
3+
import org.gradle.api.Project
4+
import org.gradle.api.file.ProjectLayout
5+
import org.gradle.api.model.ObjectFactory
6+
import javax.inject.Inject
7+
8+
open class SupportedTracerConfigurations @Inject constructor(objects: ObjectFactory, layout: ProjectLayout, project: Project) {
9+
val configOwnerPath = objects.property<String>(String::class.java).convention(":utils:config-utils")
10+
val className = objects.property<String>(String::class.java).convention("datadog.trace.config.inversion.GeneratedSupportedConfigurations")
11+
12+
val jsonFile = objects.fileProperty().convention(project.rootProject.layout.projectDirectory.file("metadata/supported-configurations.json"))
13+
14+
val destinationDirectory = objects.directoryProperty().convention(layout.buildDirectory.dir("generated/supportedConfigurations"))
15+
}

dd-java-agent/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,7 @@ dependencies {
302302
sharedShadowInclude project(':remote-config:remote-config-core'), {
303303
transitive = false
304304
}
305+
305306
sharedShadowInclude project(':utils:container-utils'), {
306307
transitive = false
307308
}

utils/config-utils/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
plugins {
22
`java-library`
3+
id("supported-config-generator")
34
}
45

56
apply(from = "$rootDir/gradle/java.gradle")

0 commit comments

Comments
 (0)