opentelemetry-java-instrume.../buildSrc/src/main/groovy/MuzzlePlugin.groovy

450 lines
19 KiB
Groovy

import org.apache.maven.repository.internal.MavenRepositorySystemUtils
import org.eclipse.aether.DefaultRepositorySystemSession
import org.eclipse.aether.RepositorySystem
import org.eclipse.aether.RepositorySystemSession
import org.eclipse.aether.artifact.Artifact
import org.eclipse.aether.artifact.DefaultArtifact
import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory
import org.eclipse.aether.impl.DefaultServiceLocator
import org.eclipse.aether.repository.LocalRepository
import org.eclipse.aether.repository.RemoteRepository
import org.eclipse.aether.resolution.VersionRangeRequest
import org.eclipse.aether.resolution.VersionRangeResult
import org.eclipse.aether.spi.connector.RepositoryConnectorFactory
import org.eclipse.aether.spi.connector.transport.TransporterFactory
import org.eclipse.aether.transport.http.HttpTransporterFactory
import org.eclipse.aether.version.Version
import org.gradle.api.Action
import org.gradle.api.GradleException
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.model.ObjectFactory
import java.lang.reflect.Method
import java.security.SecureClassLoader
import java.util.concurrent.atomic.AtomicReference
/**
* muzzle task plugin which runs muzzle validation against a range of dependencies.
*/
class MuzzlePlugin implements Plugin<Project> {
/**
* Remote repositories used to query version ranges and fetch dependencies
*/
private static final List<RemoteRepository> MUZZLE_REPOS
private static final AtomicReference<ClassLoader> TOOLING_LOADER = new AtomicReference<>()
static {
RemoteRepository central = new RemoteRepository.Builder("central", "default", "https://repo1.maven.org/maven2/").build()
MUZZLE_REPOS = new ArrayList<RemoteRepository>(Arrays.asList(central))
}
@Override
void apply(Project project) {
def bootstrapProject = project.rootProject.getChildProjects().get('dd-java-agent').getChildProjects().get('agent-bootstrap')
def toolingProject = project.rootProject.getChildProjects().get('dd-java-agent').getChildProjects().get('agent-tooling')
project.extensions.create("muzzle", MuzzleExtension, project.objects)
// compileMuzzle compiles all projects required to run muzzle validation.
// Not adding group and description to keep this task from showing in `gradle tasks`.
def compileMuzzle = project.task('compileMuzzle')
def muzzle = project.task('muzzle') {
group = 'Muzzle'
description = "Run instrumentation muzzle on compile time dependencies"
doLast {
if (!project.muzzle.directives.any { it.assertPass }) {
project.getLogger().info('No muzzle pass directives configured. Asserting pass against instrumentation compile-time dependencies')
final ClassLoader userCL = createCompileDepsClassLoader(project, bootstrapProject)
final ClassLoader instrumentationCL = createInstrumentationClassloader(project, toolingProject)
Method assertionMethod = instrumentationCL.loadClass('datadog.trace.agent.tooling.muzzle.MuzzleVersionScanPlugin')
.getMethod('assertInstrumentationMuzzled', ClassLoader.class, ClassLoader.class, boolean.class)
assertionMethod.invoke(null, instrumentationCL, userCL, true)
}
println "Muzzle executing for $project"
}
}
def printReferences = project.task('printReferences') {
group = 'Muzzle'
description = "Print references created by instrumentation muzzle"
doLast {
final ClassLoader instrumentationCL = createInstrumentationClassloader(project, toolingProject)
Method assertionMethod = instrumentationCL.loadClass('datadog.trace.agent.tooling.muzzle.MuzzleVersionScanPlugin')
.getMethod('printMuzzleReferences', ClassLoader.class)
assertionMethod.invoke(null, instrumentationCL)
}
}
project.tasks.compileMuzzle.dependsOn(bootstrapProject.tasks.compileJava)
project.tasks.compileMuzzle.dependsOn(toolingProject.tasks.compileJava)
project.afterEvaluate {
project.tasks.compileMuzzle.dependsOn(project.tasks.compileJava)
if (project.tasks.getNames().contains('compileScala')) {
project.tasks.compileMuzzle.dependsOn(project.tasks.compileScala)
}
}
project.tasks.muzzle.dependsOn(project.tasks.compileMuzzle)
project.tasks.printReferences.dependsOn(project.tasks.compileMuzzle)
def hasRelevantTask = project.gradle.startParameter.taskNames.any { taskName ->
// removing leading ':' if present
taskName = taskName.replaceFirst('^:', '')
String muzzleTaskPath = project.path.replaceFirst('^:', '')
return 'muzzle' == taskName || "${muzzleTaskPath}:muzzle" == taskName
}
if (!hasRelevantTask) {
// Adding muzzle dependencies has a large config overhead. Stop unless muzzle is explicitly run.
return
}
final RepositorySystem system = newRepositorySystem()
final RepositorySystemSession session = newRepositorySystemSession(system)
project.afterEvaluate {
// use runAfter to set up task finalizers in version order
Task runAfter = project.tasks.muzzle
for (MuzzleDirective muzzleDirective : project.muzzle.directives) {
project.getLogger().info("configured $muzzleDirective")
if (muzzleDirective.coreJdk) {
runAfter = addMuzzleTask(muzzleDirective, null, project, runAfter, bootstrapProject, toolingProject)
} else {
muzzleDirectiveToArtifacts(muzzleDirective, system, session).collect() { Artifact singleVersion ->
runAfter = addMuzzleTask(muzzleDirective, singleVersion, project, runAfter, bootstrapProject, toolingProject)
}
if (muzzleDirective.assertInverse) {
inverseOf(muzzleDirective, system, session).collect() { MuzzleDirective inverseDirective ->
muzzleDirectiveToArtifacts(inverseDirective, system, session).collect() { Artifact singleVersion ->
runAfter = addMuzzleTask(inverseDirective, singleVersion, project, runAfter, bootstrapProject, toolingProject)
}
}
}
}
}
}
}
private static ClassLoader getOrCreateToolingLoader(Project toolingProject) {
synchronized (TOOLING_LOADER) {
final ClassLoader toolingLoader = TOOLING_LOADER.get()
if (toolingLoader == null) {
Set<URL> ddUrls = new HashSet<>()
toolingProject.getLogger().info('creating classpath for agent-tooling')
for (File f : toolingProject.sourceSets.main.runtimeClasspath.getFiles()) {
toolingProject.getLogger().info('--' + f)
ddUrls.add(f.toURI().toURL())
}
def loader = new URLClassLoader(ddUrls.toArray(new URL[0]), (ClassLoader) null)
assert TOOLING_LOADER.compareAndSet(null, loader)
loader.loadClass("datadog.trace.agent.tooling.AgentTooling").getMethod("init").invoke(null)
return TOOLING_LOADER.get()
} else {
return toolingLoader
}
}
}
/**
* Create a classloader with core agent classes and project instrumentation on the classpath.
*/
private static ClassLoader createInstrumentationClassloader(Project project, Project toolingProject) {
project.getLogger().info("Creating instrumentation classpath for: " + project.getName())
Set<URL> ddUrls = new HashSet<>()
for (File f : project.sourceSets.main.runtimeClasspath.getFiles()) {
project.getLogger().info('--' + f)
ddUrls.add(f.toURI().toURL())
}
return new URLClassLoader(ddUrls.toArray(new URL[0]), getOrCreateToolingLoader(toolingProject))
}
/**
* Create a classloader with all compile-time dependencies on the classpath
*/
private static ClassLoader createCompileDepsClassLoader(Project project, Project bootstrapProject) {
List<URL> userUrls = new ArrayList<>()
project.getLogger().info("Creating compile-time classpath for: " + project.getName())
for (File f : project.configurations.compileOnly.getFiles()) {
project.getLogger().info('--' + f)
userUrls.add(f.toURI().toURL())
}
for (File f : bootstrapProject.sourceSets.main.runtimeClasspath.getFiles()) {
project.getLogger().info('--' + f)
userUrls.add(f.toURI().toURL())
}
return new URLClassLoader(userUrls.toArray(new URL[0]), (ClassLoader) null)
}
/**
* Create a classloader with dependencies for a single muzzle task.
*/
private static ClassLoader createClassLoaderForTask(Project project, Project bootstrapProject, String muzzleTaskName) {
final List<URL> userUrls = new ArrayList<>()
project.getLogger().info("Creating task classpath")
project.configurations.getByName(muzzleTaskName).resolvedConfiguration.files.each { File jarFile ->
project.getLogger().info("-- Added to instrumentation classpath: $jarFile")
userUrls.add(jarFile.toURI().toURL())
}
for (File f : bootstrapProject.sourceSets.main.runtimeClasspath.getFiles()) {
project.getLogger().info("-- Added to instrumentation bootstrap classpath: $f")
userUrls.add(f.toURI().toURL())
}
return new URLClassLoader(userUrls.toArray(new URL[0]), (ClassLoader) null)
}
/**
* Convert a muzzle directive to a list of artifacts
*/
private static List<Artifact> muzzleDirectiveToArtifacts(MuzzleDirective muzzleDirective, RepositorySystem system, RepositorySystemSession session) {
final Artifact directiveArtifact = new DefaultArtifact(muzzleDirective.group, muzzleDirective.module, "jar", muzzleDirective.versions)
final VersionRangeRequest rangeRequest = new VersionRangeRequest()
rangeRequest.setRepositories(MUZZLE_REPOS)
rangeRequest.setArtifact(directiveArtifact)
final VersionRangeResult rangeResult = system.resolveVersionRange(session, rangeRequest)
final List<Artifact> allVersionArtifacts = filterVersion(rangeResult.versions).collect { version ->
new DefaultArtifact(muzzleDirective.group, muzzleDirective.module, "jar", version.toString())
}
if (allVersionArtifacts.isEmpty()) {
throw new GradleException("No muzzle artifacts found for $muzzleDirective.group:$muzzleDirective.module $muzzleDirective.versions")
}
return allVersionArtifacts
}
/**
* Create a list of muzzle directives which assert the opposite of the given MuzzleDirective.
*/
private static List<MuzzleDirective> inverseOf(MuzzleDirective muzzleDirective, RepositorySystem system, RepositorySystemSession session) {
List<MuzzleDirective> inverseDirectives = new ArrayList<>()
final Artifact allVerisonsArtifact = new DefaultArtifact(muzzleDirective.group, muzzleDirective.module, "jar", "[,)")
final Artifact directiveArtifact = new DefaultArtifact(muzzleDirective.group, muzzleDirective.module, "jar", muzzleDirective.versions)
final VersionRangeRequest allRangeRequest = new VersionRangeRequest()
allRangeRequest.setRepositories(MUZZLE_REPOS)
allRangeRequest.setArtifact(allVerisonsArtifact)
final VersionRangeResult allRangeResult = system.resolveVersionRange(session, allRangeRequest)
final VersionRangeRequest rangeRequest = new VersionRangeRequest()
rangeRequest.setRepositories(MUZZLE_REPOS)
rangeRequest.setArtifact(directiveArtifact)
final VersionRangeResult rangeResult = system.resolveVersionRange(session, rangeRequest)
filterVersion(allRangeResult.versions).collect { version ->
if (!rangeResult.versions.contains(version)) {
final MuzzleDirective inverseDirective = new MuzzleDirective()
inverseDirective.group = muzzleDirective.group
inverseDirective.module = muzzleDirective.module
inverseDirective.versions = "$version"
inverseDirective.assertPass = !muzzleDirective.assertPass
inverseDirectives.add(inverseDirective)
}
}
return inverseDirectives
}
/**
* Configure a muzzle task to pass or fail a given version.
*
* @param assertPass If true, assert that muzzle validation passes
* @param versionArtifact version to assert against.
* @param instrumentationProject instrumentation being asserted against.
* @param runAfter Task which runs before the new muzzle task.
* @param bootstrapProject Agent bootstrap project.
* @param toolingProject Agent tooling project.
*
* @return The created muzzle task.
*/
private static Task addMuzzleTask(MuzzleDirective muzzleDirective, Artifact versionArtifact, Project instrumentationProject, Task runAfter, Project bootstrapProject, Project toolingProject) {
def taskName
if (muzzleDirective.coreJdk) {
taskName = "muzzle-Assert$muzzleDirective"
} else {
taskName = "muzzle-Assert${muzzleDirective.assertPass ? "Pass" : "Fail"}-$versionArtifact.groupId-$versionArtifact.artifactId-$versionArtifact.version${muzzleDirective.name ? "-${muzzleDirective.getNameSlug()}" : ""}"
}
def config = instrumentationProject.configurations.create(taskName)
if (!muzzleDirective.coreJdk) {
def dep = instrumentationProject.dependencies.create("$versionArtifact.groupId:$versionArtifact.artifactId:$versionArtifact.version") {
transitive = true
}
// The following optional transitive dependencies are brought in by some legacy module such as log4j 1.x but are no
// longer bundled with the JVM and have to be excluded for the muzzle tests to be able to run.
dep.exclude group: 'com.sun.jdmk', module: 'jmxtools'
dep.exclude group: 'com.sun.jmx', module: 'jmxri'
config.dependencies.add(dep)
}
for (String additionalDependency : muzzleDirective.additionalDependencies) {
config.dependencies.add(instrumentationProject.dependencies.create(additionalDependency) {
transitive = true
})
}
def muzzleTask = instrumentationProject.task(taskName) {
doLast {
final ClassLoader instrumentationCL = createInstrumentationClassloader(instrumentationProject, toolingProject)
def ccl = Thread.currentThread().contextClassLoader
def bogusLoader = new SecureClassLoader() {
@Override
String toString() { return "bogus" }
}
Thread.currentThread().contextClassLoader = bogusLoader
final ClassLoader userCL = createClassLoaderForTask(instrumentationProject, bootstrapProject, taskName)
try {
// find all instrumenters, get muzzle, and assert
Method assertionMethod = instrumentationCL.loadClass('datadog.trace.agent.tooling.muzzle.MuzzleVersionScanPlugin')
.getMethod('assertInstrumentationMuzzled', ClassLoader.class, ClassLoader.class, boolean.class)
assertionMethod.invoke(null, instrumentationCL, userCL, muzzleDirective.assertPass)
} finally {
Thread.currentThread().contextClassLoader = ccl
}
for (Thread thread : Thread.getThreads()) {
if (thread.contextClassLoader == bogusLoader || thread.contextClassLoader == instrumentationCL || thread.contextClassLoader == userCL) {
throw new GradleException("Task $taskName has spawned a thread: $thread with classloader $thread.contextClassLoader. This will prevent GC of dynamic muzzle classes. Aborting muzzle run.")
}
}
}
}
runAfter.finalizedBy(muzzleTask)
return muzzleTask
}
/**
* Create muzzle's repository system
*/
private static RepositorySystem newRepositorySystem() {
DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator()
locator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class)
locator.addService(TransporterFactory.class, HttpTransporterFactory.class)
return locator.getService(RepositorySystem.class)
}
/**
* Create muzzle's repository system session
*/
private static RepositorySystemSession newRepositorySystemSession(RepositorySystem system) {
DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession()
def tempDir = File.createTempDir()
tempDir.deleteOnExit()
LocalRepository localRepo = new LocalRepository(tempDir)
session.setLocalRepositoryManager(system.newLocalRepositoryManager(session, localRepo))
return session
}
/**
* Filter out snapshot-type builds from versions list.
*/
private static filterVersion(List<Version> list) {
list.removeIf {
def version = it.toString().toLowerCase()
return version.contains("rc") ||
version.contains(".cr") ||
version.contains("alpha") ||
version.contains("beta") ||
version.contains("-b") ||
version.contains(".m") ||
version.contains("-m") ||
version.contains("-dev") ||
version.contains("public_draft")
}
return list
}
}
// plugin extension classes
/**
* A pass or fail directive for a single dependency.
*/
class MuzzleDirective {
/**
* Name is optional and is used to further define the scope of a directive. The motivation for this is that this
* plugin creates a config for each of the dependencies under test with name '...-<group_id>-<artifact_id>-<version>'.
* The problem is that if we want to test multiple times the same configuration under different conditions, e.g.
* with different extra dependencies, the plugin would throw an error as it would try to create several times the
* same config. This property can be used to differentiate those config names for different directives.
*/
String name
String group
String module
String versions
List<String> additionalDependencies = new ArrayList<>()
boolean assertPass
boolean assertInverse = false
boolean coreJdk = false
void coreJdk() {
coreJdk = true
}
/**
* Adds extra dependencies to the current muzzle test.
*
* @param compileString An extra dependency in the gradle canonical form: '<group_id>:<artifact_id>:<version_id>'.
*/
void extraDependency(String compileString) {
additionalDependencies.add(compileString)
}
/**
* Slug of directive name.
*
* @return A slug of the name or an empty string if name is empty. E.g. 'My Directive' --> 'My-Directive'
*/
String getNameSlug() {
if (null == name) {
return ""
}
return name.trim().replaceAll("[^a-zA-Z0-9]+", "-")
}
String toString() {
if (coreJdk) {
return "${assertPass ? 'Pass' : 'Fail'}-core-jdk"
} else {
return "${assertPass ? 'pass' : 'fail'} $group:$module:$versions"
}
}
}
/**
* Muzzle extension containing all pass and fail directives.
*/
class MuzzleExtension {
final List<MuzzleDirective> directives = new ArrayList<>()
private final ObjectFactory objectFactory
@javax.inject.Inject
MuzzleExtension(final ObjectFactory objectFactory) {
this.objectFactory = objectFactory
}
void pass(Action<? super MuzzleDirective> action) {
final MuzzleDirective pass = objectFactory.newInstance(MuzzleDirective)
action.execute(pass)
pass.assertPass = true
directives.add(pass)
}
void fail(Action<? super MuzzleDirective> action) {
final MuzzleDirective fail = objectFactory.newInstance(MuzzleDirective)
action.execute(fail)
fail.assertPass = false
directives.add(fail)
}
}