Migrate MuzzlePlugin to Java (#3017)

This commit is contained in:
Anuraag Agrawal 2021-05-18 12:52:55 +09:00 committed by GitHub
parent 1535834d46
commit f3191d9e00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 572 additions and 418 deletions

View File

@ -1,5 +1,4 @@
plugins {
id 'groovy'
id 'java-gradle-plugin'
id "com.diffplug.spotless" version "5.12.4"
}
@ -10,16 +9,13 @@ spotless {
licenseHeaderFile rootProject.file('../gradle/enforcement/spotless.license.java'), '(package|import|public)'
target 'src/**/*.java'
}
groovy {
licenseHeaderFile rootProject.file('../gradle/enforcement/spotless.license.java'), '(package|import|class)'
}
}
gradlePlugin {
plugins {
create("muzzle-plugin") {
id = "muzzle"
implementationClass = "MuzzlePlugin"
implementationClass = "io.opentelemetry.instrumentation.gradle.muzzle.MuzzlePlugin"
}
}
}

View File

@ -1,408 +0,0 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
import io.opentelemetry.instrumentation.gradle.muzzle.MuzzleDirective
import io.opentelemetry.instrumentation.gradle.muzzle.MuzzleExtension
import java.lang.reflect.Method
import java.security.SecureClassLoader
import java.util.concurrent.atomic.AtomicReference
import java.util.function.Predicate
import java.util.regex.Pattern
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.GradleException
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.tasks.TaskProvider
/**
* muzzle task plugin which runs muzzle validation against a range of dependencies.
*/
class MuzzlePlugin implements Plugin<Project> {
/**
* Select a random set of versions to test
*/
private static final int RANGE_COUNT_LIMIT = 10
private static final AtomicReference<ClassLoader> TOOLING_LOADER = new AtomicReference<>()
@Override
void apply(Project project) {
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.tasks.register('compileMuzzle') {
dependsOn(':javaagent-bootstrap:classes')
dependsOn(':javaagent-tooling:classes')
dependsOn(':javaagent-extension-api:classes')
dependsOn(project.tasks.classes)
}
def muzzle = project.tasks.register('muzzle') {
group = 'Muzzle'
description = "Run instrumentation muzzle on compile time dependencies"
dependsOn(compileMuzzle)
}
project.tasks.register('printMuzzleReferences') {
group = 'Muzzle'
description = "Print references created by instrumentation muzzle"
dependsOn(compileMuzzle)
doLast {
ClassLoader instrumentationCL = createInstrumentationClassloader(project)
Method assertionMethod = instrumentationCL.loadClass('io.opentelemetry.javaagent.tooling.muzzle.matcher.MuzzleGradlePluginUtil')
.getMethod('printMuzzleReferences', ClassLoader.class)
assertionMethod.invoke(null, instrumentationCL)
}
}
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
}
RepositorySystem system = newRepositorySystem()
RepositorySystemSession session = newRepositorySystemSession(system)
project.afterEvaluate {
// use runAfter to set up task finalizers in version order
TaskProvider runAfter = muzzle
for (MuzzleDirective muzzleDirective : project.muzzle.directives.get()) {
project.getLogger().info("configured $muzzleDirective")
if (muzzleDirective.coreJdk.get()) {
runAfter = addMuzzleTask(muzzleDirective, null, project, runAfter)
} else {
muzzleDirectiveToArtifacts(project, muzzleDirective, system, session).collect() { Artifact singleVersion ->
runAfter = addMuzzleTask(muzzleDirective, singleVersion, project, runAfter)
}
if (muzzleDirective.assertInverse.get()) {
inverseOf(project, muzzleDirective, system, session).collect() { MuzzleDirective inverseDirective ->
muzzleDirectiveToArtifacts(project, inverseDirective, system, session).collect() { Artifact singleVersion ->
runAfter = addMuzzleTask(inverseDirective, singleVersion, project, runAfter)
}
}
}
}
}
}
}
private static ClassLoader getOrCreateToolingLoader(Project project) {
synchronized (TOOLING_LOADER) {
ClassLoader toolingLoader = TOOLING_LOADER.get()
if (toolingLoader == null) {
Set<URL> urls = new HashSet<>()
project.getLogger().info('creating classpath for auto-tooling')
for (File f : project.configurations.toolingRuntime.getFiles()) {
project.getLogger().info('--' + f)
urls.add(f.toURI().toURL())
}
def loader = new URLClassLoader(urls.toArray(new URL[0]), ClassLoader.platformClassLoader)
assert TOOLING_LOADER.compareAndSet(null, loader)
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.getLogger().info("Creating instrumentation classpath for: " + project.getName())
Set<URL> urls = new HashSet<>()
for (File f : project.sourceSets.main.runtimeClasspath.getFiles()) {
project.getLogger().info('--' + f)
urls.add(f.toURI().toURL())
}
return new URLClassLoader(urls.toArray(new URL[0]), getOrCreateToolingLoader(project))
}
/**
* Create a classloader with all compile-time dependencies on the classpath
*/
private static ClassLoader createCompileDepsClassLoader(Project project) {
List<URL> userUrls = new ArrayList<>()
project.getLogger().info("Creating compile-time classpath for: " + project.getName())
for (File f : project.configurations.compileClasspath.getFiles()) {
project.getLogger().info('--' + f)
userUrls.add(f.toURI().toURL())
}
for (File f : project.configurations.bootstrapRuntime.getFiles()) {
project.getLogger().info('--' + f)
userUrls.add(f.toURI().toURL())
}
return new URLClassLoader(userUrls.toArray(new URL[0]), ClassLoader.platformClassLoader)
}
/**
* Create a classloader with dependencies for a single muzzle task.
*/
private static ClassLoader createClassLoaderForTask(Project project, String muzzleTaskName) {
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 : project.configurations.bootstrapRuntime.getFiles()) {
project.getLogger().info("-- Added to instrumentation bootstrap classpath: $f")
userUrls.add(f.toURI().toURL())
}
return new URLClassLoader(userUrls.toArray(new URL[0]), ClassLoader.platformClassLoader)
}
/**
* Convert a muzzle directive to a list of artifacts
*/
private static Set<Artifact> muzzleDirectiveToArtifacts(Project instrumentationProject, MuzzleDirective muzzleDirective, RepositorySystem system, RepositorySystemSession session) {
Artifact directiveArtifact = new DefaultArtifact(muzzleDirective.group.get(), muzzleDirective.module.get(), "jar", muzzleDirective.versions.get())
VersionRangeRequest rangeRequest = new VersionRangeRequest()
rangeRequest.setRepositories(getProjectRepositories(instrumentationProject))
rangeRequest.setArtifact(directiveArtifact)
VersionRangeResult rangeResult = system.resolveVersionRange(session, rangeRequest)
Set<Artifact> allVersionArtifacts = filterVersions(rangeResult, muzzleDirective.normalizedSkipVersions).collect { version ->
new DefaultArtifact(muzzleDirective.group.get(), muzzleDirective.module.get(), "jar", version)
}.toSet()
if (allVersionArtifacts.isEmpty()) {
throw new GradleException("No muzzle artifacts found for $muzzleDirective")
}
return allVersionArtifacts
}
private static List<RemoteRepository> getProjectRepositories(Project project) {
project.repositories.collect {
new RemoteRepository.Builder(it.name, "default", it.url.toString()).build()
}
}
/**
* Create a list of muzzle directives which assert the opposite of the given MuzzleDirective.
*/
private static Set<MuzzleDirective> inverseOf(Project instrumentationProject, MuzzleDirective muzzleDirective, RepositorySystem system, RepositorySystemSession session) {
Set<MuzzleDirective> inverseDirectives = new HashSet<>()
Artifact allVersionsArtifact = new DefaultArtifact(muzzleDirective.group.get(), muzzleDirective.module.get(), "jar", "[,)")
Artifact directiveArtifact = new DefaultArtifact(muzzleDirective.group.get(), muzzleDirective.module.get(), "jar", muzzleDirective.versions.get())
List<RemoteRepository> repos = getProjectRepositories(instrumentationProject)
VersionRangeRequest allRangeRequest = new VersionRangeRequest()
allRangeRequest.setRepositories(repos)
allRangeRequest.setArtifact(allVersionsArtifact)
VersionRangeResult allRangeResult = system.resolveVersionRange(session, allRangeRequest)
VersionRangeRequest rangeRequest = new VersionRangeRequest()
rangeRequest.setRepositories(repos)
rangeRequest.setArtifact(directiveArtifact)
VersionRangeResult rangeResult = system.resolveVersionRange(session, rangeRequest)
allRangeResult.getVersions().removeAll(rangeResult.getVersions())
filterVersions(allRangeResult, muzzleDirective.normalizedSkipVersions).each { version ->
MuzzleDirective inverseDirective = instrumentationProject.objects.newInstance(MuzzleDirective)
inverseDirective.group = muzzleDirective.group
inverseDirective.module = muzzleDirective.module
inverseDirective.versions = version
inverseDirective.assertPass = !muzzleDirective.assertPass
inverseDirectives.add(inverseDirective)
}
return inverseDirectives
}
private static Set<String> filterVersions(VersionRangeResult range, Set<String> skipVersions) {
Set<String> result = new HashSet<>()
def predicate = new AcceptableVersions(range, skipVersions)
if (predicate.test(range.lowestVersion)) {
result.add(range.lowestVersion.toString())
}
if (predicate.test(range.highestVersion)) {
result.add(range.highestVersion.toString())
}
List<Version> copy = new ArrayList<>(range.versions)
Collections.shuffle(copy)
while (result.size() < RANGE_COUNT_LIMIT && !copy.isEmpty()) {
Version version = copy.pop()
if (predicate.test(version)) {
result.add(version.toString())
}
}
return result
}
static class AcceptableVersions implements Predicate<Version> {
private static final Pattern GIT_SHA_PATTERN = Pattern.compile('^.*-[0-9a-f]{7,}$')
private final VersionRangeResult range
private final Collection<String> skipVersions
AcceptableVersions(VersionRangeResult range, Collection<String> skipVersions) {
this.range = range
this.skipVersions = skipVersions
}
@Override
boolean test(Version version) {
if (version == null) {
return false
}
def versionString = version.toString().toLowerCase()
if (skipVersions.contains(versionString)) {
return false
}
def draftVersion = versionString.contains("rc") ||
versionString.contains(".cr") ||
versionString.contains("alpha") ||
versionString.contains("beta") ||
versionString.contains("-b") ||
versionString.contains(".m") ||
versionString.contains("-m") ||
versionString.contains("-dev") ||
versionString.contains("-ea") ||
versionString.contains("-atlassian-") ||
versionString.contains("public_draft") ||
versionString.contains("snapshot") ||
versionString.matches(GIT_SHA_PATTERN)
return !draftVersion
}
}
/**
* 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.
*
* @return The created muzzle task.
*/
private static TaskProvider addMuzzleTask(MuzzleDirective muzzleDirective, Artifact versionArtifact, Project instrumentationProject, TaskProvider runAfter) {
def taskName
if (muzzleDirective.coreJdk.get()) {
taskName = "muzzle-Assert$muzzleDirective"
} else {
taskName = "muzzle-Assert${muzzleDirective.assertPass ? "Pass" : "Fail"}-$versionArtifact.groupId-$versionArtifact.artifactId-$versionArtifact.version${!muzzleDirective.name.get().isEmpty() ? "-${muzzleDirective.getNameSlug()}" : ""}"
}
def config = instrumentationProject.configurations.create(taskName)
if (!muzzleDirective.coreJdk.get()) {
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.get()) {
if (additionalDependency.count(":") < 2) {
// Dependency definition without version, use the artifact's version.
additionalDependency += ":${versionArtifact.version}"
}
config.dependencies.add(instrumentationProject.dependencies.create(additionalDependency) {
transitive = true
})
}
def muzzleTask = instrumentationProject.tasks.register(taskName) {
dependsOn(instrumentationProject.configurations.named("runtimeClasspath"))
doLast {
ClassLoader instrumentationCL = createInstrumentationClassloader(instrumentationProject)
def ccl = Thread.currentThread().contextClassLoader
def bogusLoader = new SecureClassLoader() {
@Override
String toString() {
return "bogus"
}
}
Thread.currentThread().contextClassLoader = bogusLoader
ClassLoader userCL = createClassLoaderForTask(instrumentationProject, taskName)
try {
// find all instrumenters, get muzzle, and assert
Method assertionMethod = instrumentationCL.loadClass('io.opentelemetry.javaagent.tooling.muzzle.matcher.MuzzleGradlePluginUtil')
.getMethod('assertInstrumentationMuzzled', ClassLoader.class, ClassLoader.class, boolean.class)
assertionMethod.invoke(null, instrumentationCL, userCL, muzzleDirective.assertPass.get())
} 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.configure {
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
}
}

View File

@ -0,0 +1,50 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.gradle.muzzle;
import java.util.Collection;
import java.util.Locale;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import org.eclipse.aether.version.Version;
class AcceptableVersions implements Predicate<Version> {
private static final Pattern GIT_SHA_PATTERN = Pattern.compile("^.*-[0-9a-f]{7,}$");
private final Collection<String> skipVersions;
AcceptableVersions(Collection<String> skipVersions) {
this.skipVersions = skipVersions;
}
@Override
public boolean test(Version version) {
if (version == null) {
return false;
}
String versionString = version.toString().toLowerCase(Locale.ROOT);
if (skipVersions.contains(versionString)) {
return false;
}
boolean draftVersion =
versionString.contains("rc")
|| versionString.contains(".cr")
|| versionString.contains("alpha")
|| versionString.contains("beta")
|| versionString.contains("-b")
|| versionString.contains(".m")
|| versionString.contains("-m")
|| versionString.contains("-dev")
|| versionString.contains("-ea")
|| versionString.contains("-atlassian-")
|| versionString.contains("public_draft")
|| versionString.contains("snapshot")
|| GIT_SHA_PATTERN.matcher(versionString).matches();
return !draftVersion;
}
}

View File

@ -0,0 +1,518 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.gradle.muzzle;
import java.io.File;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.SecureClassLoader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
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.VersionRangeResolutionException;
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.GradleException;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.ModuleDependency;
import org.gradle.api.artifacts.repositories.MavenArtifactRepository;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.FileCollection;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.SourceSetContainer;
import org.gradle.api.tasks.TaskProvider;
public class MuzzlePlugin implements Plugin<Project> {
/** Select a random set of versions to test */
private static final int RANGE_COUNT_LIMIT = 10;
private static volatile ClassLoader TOOLING_LOADER;
@Override
public void apply(Project project) {
MuzzleExtension muzzleConfig =
project.getExtensions().create("muzzle", MuzzleExtension.class, project.getObjects());
// compileMuzzle compiles all projects required to run muzzle validation.
// Not adding group and description to keep this task from showing in `gradle tasks`.
TaskProvider<?> compileMuzzle =
project
.getTasks()
.register(
"compileMuzzle",
task -> {
task.dependsOn(":javaagent-bootstrap:classes");
task.dependsOn(":javaagent-tooling:classes");
task.dependsOn(":javaagent-extension-api:classes");
task.dependsOn(project.getTasks().named(JavaPlugin.CLASSES_TASK_NAME));
});
TaskProvider<?> muzzle =
project
.getTasks()
.register(
"muzzle",
task -> {
task.setGroup("Muzzle");
task.setDescription("Run instrumentation muzzle on compile time dependencies");
task.dependsOn(compileMuzzle);
});
project
.getTasks()
.register(
"printMuzzleReferences",
task -> {
task.setGroup("Muzzle");
task.setDescription("Print references created by instrumentation muzzle");
task.dependsOn(compileMuzzle);
task.doLast(
unused -> {
ClassLoader instrumentationCL = createInstrumentationClassloader(project);
try {
Method assertionMethod =
instrumentationCL
.loadClass(
"io.opentelemetry.javaagent.tooling.muzzle.matcher.MuzzleGradlePluginUtil")
.getMethod("printMuzzleReferences", ClassLoader.class);
assertionMethod.invoke(null, instrumentationCL);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
});
boolean hasRelevantTask =
project.getGradle().getStartParameter().getTaskNames().stream()
.anyMatch(
taskName -> {
// removing leading ':' if present
if (taskName.startsWith(":")) {
taskName = taskName.substring(1);
}
String projectPath = project.getPath().substring(1);
// Either the specific muzzle task in this project or the top level, full-project
// muzzle task.
return taskName.equals(projectPath + ":muzzle") || taskName.equals("muzzle");
});
if (!hasRelevantTask) {
// Adding muzzle dependencies has a large config overhead. Stop unless muzzle is explicitly
// run.
return;
}
RepositorySystem system = newRepositorySystem();
RepositorySystemSession session = newRepositorySystemSession(system, project);
project.afterEvaluate(
unused -> {
// use runAfter to set up task finalizers in version order
TaskProvider<?> runAfter = muzzle;
for (MuzzleDirective muzzleDirective : muzzleConfig.getDirectives().get()) {
project.getLogger().info("configured " + muzzleDirective);
if (muzzleDirective.getCoreJdk().get()) {
runAfter = addMuzzleTask(muzzleDirective, null, project, runAfter);
} else {
for (Artifact singleVersion :
muzzleDirectiveToArtifacts(project, muzzleDirective, system, session)) {
runAfter = addMuzzleTask(muzzleDirective, singleVersion, project, runAfter);
}
if (muzzleDirective.getAssertInverse().get()) {
for (MuzzleDirective inverseDirective :
inverseOf(project, muzzleDirective, system, session)) {
for (Artifact singleVersion :
muzzleDirectiveToArtifacts(project, inverseDirective, system, session)) {
runAfter = addMuzzleTask(inverseDirective, singleVersion, project, runAfter);
}
}
}
}
}
});
}
/** Create a classloader with core agent classes and project instrumentation on the classpath. */
private static ClassLoader createInstrumentationClassloader(Project project) {
project.getLogger().info("Creating instrumentation classpath for: " + project.getName());
SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class);
FileCollection runtimeClasspath =
sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME).getRuntimeClasspath();
return classpathLoader(runtimeClasspath, getOrCreateToolingLoader(project), project);
}
private static synchronized ClassLoader getOrCreateToolingLoader(Project project) {
if (TOOLING_LOADER == null) {
project.getLogger().info("creating classpath for auto-tooling");
FileCollection toolingRuntime = project.getConfigurations().getByName("toolingRuntime");
TOOLING_LOADER =
classpathLoader(toolingRuntime, ClassLoader.getPlatformClassLoader(), project);
}
return TOOLING_LOADER;
}
private static ClassLoader classpathLoader(
FileCollection classpath, ClassLoader parent, Project project) {
URL[] urls =
StreamSupport.stream(classpath.spliterator(), false)
.map(
file -> {
project.getLogger().info("--" + file);
try {
return file.toURI().toURL();
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
})
.toArray(URL[]::new);
return new URLClassLoader(urls, parent);
}
/**
* Configure a muzzle task to pass or fail a given version.
*
* @param versionArtifact version to assert against.
* @param instrumentationProject instrumentation being asserted against.
* @param runAfter Task which runs before the new muzzle task.
* @return The created muzzle task.
*/
private static TaskProvider addMuzzleTask(
MuzzleDirective muzzleDirective,
Artifact versionArtifact,
Project instrumentationProject,
TaskProvider<?> runAfter) {
final String taskName;
if (muzzleDirective.getCoreJdk().get()) {
taskName = "muzzle-Assert" + muzzleDirective;
} else {
StringBuilder sb = new StringBuilder("muzzle-Assert");
if (muzzleDirective.getAssertPass().isPresent()) {
sb.append("Pass");
} else {
sb.append("Fail");
}
sb.append('-')
.append(versionArtifact.getGroupId())
.append('-')
.append(versionArtifact.getArtifactId())
.append('-')
.append(versionArtifact.getVersion());
if (!muzzleDirective.getName().get().isEmpty()) {
sb.append(muzzleDirective.getNameSlug());
}
taskName = sb.toString();
}
Configuration config = instrumentationProject.getConfigurations().create(taskName);
if (!muzzleDirective.getCoreJdk().get()) {
ModuleDependency dep =
(ModuleDependency)
instrumentationProject
.getDependencies()
.create(
versionArtifact.getGroupId()
+ ':'
+ versionArtifact.getArtifactId()
+ ':'
+ versionArtifact.getVersion());
dep.setTransitive(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.
exclude(dep, "com.sun.jdmk", "jmxtools");
exclude(dep, "com.sun.jmx", "jmxri");
config.getDependencies().add(dep);
}
for (String additionalDependency : muzzleDirective.getAdditionalDependencies().get()) {
if (countColons(additionalDependency) < 2) {
// Dependency definition without version, use the artifact's version.
additionalDependency = additionalDependency + ':' + versionArtifact.getVersion();
}
ModuleDependency dep =
(ModuleDependency) instrumentationProject.getDependencies().create(additionalDependency);
dep.setTransitive(true);
config.getDependencies().add(dep);
}
TaskProvider<?> muzzleTask =
instrumentationProject
.getTasks()
.register(
taskName,
task -> {
task.dependsOn(
instrumentationProject.getConfigurations().named("runtimeClasspath"));
task.doLast(
unused -> {
ClassLoader instrumentationCL =
createInstrumentationClassloader(instrumentationProject);
ClassLoader ccl = Thread.currentThread().getContextClassLoader();
ClassLoader bogusLoader =
new SecureClassLoader() {
@Override
public String toString() {
return "bogus";
}
};
Thread.currentThread().setContextClassLoader(bogusLoader);
ClassLoader userCL =
createClassLoaderForTask(instrumentationProject, taskName);
try {
// find all instrumenters, get muzzle, and assert
Method assertionMethod =
instrumentationCL
.loadClass(
"io.opentelemetry.javaagent.tooling.muzzle.matcher.MuzzleGradlePluginUtil")
.getMethod(
"assertInstrumentationMuzzled",
ClassLoader.class,
ClassLoader.class,
boolean.class);
assertionMethod.invoke(
null,
instrumentationCL,
userCL,
muzzleDirective.getAssertPass().get());
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
Thread.currentThread().setContextClassLoader(ccl);
}
for (Thread thread : Thread.getAllStackTraces().keySet()) {
if (thread.getContextClassLoader() == bogusLoader
|| thread.getContextClassLoader() == instrumentationCL
|| thread.getContextClassLoader() == userCL) {
throw new GradleException(
"Task "
+ taskName
+ " has spawned a thread: "
+ thread
+ " with classloader "
+ thread.getContextClassLoader()
+ ". This will prevent GC of dynamic muzzle classes. Aborting muzzle run.");
}
}
});
});
runAfter.configure(task -> task.finalizedBy(muzzleTask));
return muzzleTask;
}
/** Create a classloader with dependencies for a single muzzle task. */
private static ClassLoader createClassLoaderForTask(Project project, String muzzleTaskName) {
ConfigurableFileCollection userUrls = project.getObjects().fileCollection();
project.getLogger().info("Creating task classpath");
userUrls.from(
project
.getConfigurations()
.getByName(muzzleTaskName)
.getResolvedConfiguration()
.getFiles());
return classpathLoader(
userUrls.plus(project.getConfigurations().getByName("bootstrapRuntime")),
ClassLoader.getPlatformClassLoader(),
project);
}
/** Convert a muzzle directive to a list of artifacts */
private static Set<Artifact> muzzleDirectiveToArtifacts(
Project instrumentationProject,
MuzzleDirective muzzleDirective,
RepositorySystem system,
RepositorySystemSession session) {
Artifact directiveArtifact =
new DefaultArtifact(
muzzleDirective.getGroup().get(),
muzzleDirective.getModule().get(),
"jar",
muzzleDirective.getVersions().get());
VersionRangeRequest rangeRequest = new VersionRangeRequest();
rangeRequest.setRepositories(getProjectRepositories(instrumentationProject));
rangeRequest.setArtifact(directiveArtifact);
final VersionRangeResult rangeResult;
try {
rangeResult = system.resolveVersionRange(session, rangeRequest);
} catch (VersionRangeResolutionException e) {
throw new RuntimeException(e);
}
Set<Artifact> allVersionArtifacts =
filterVersions(rangeResult, muzzleDirective.getNormalizedSkipVersions()).stream()
.map(
version ->
new DefaultArtifact(
muzzleDirective.getGroup().get(),
muzzleDirective.getModule().get(),
"jar",
version))
.collect(Collectors.toSet());
if (allVersionArtifacts.isEmpty()) {
throw new GradleException("No muzzle artifacts found for " + muzzleDirective);
}
return allVersionArtifacts;
}
private static List<RemoteRepository> getProjectRepositories(Project project) {
return project.getRepositories().stream()
.filter(MavenArtifactRepository.class::isInstance)
.map(
repo -> {
MavenArtifactRepository mavenRepo = (MavenArtifactRepository) repo;
return new RemoteRepository.Builder(
mavenRepo.getName(), "default", mavenRepo.getUrl().toString())
.build();
})
.collect(Collectors.toList());
}
/** Create a list of muzzle directives which assert the opposite of the given MuzzleDirective. */
private static Set<MuzzleDirective> inverseOf(
Project instrumentationProject,
MuzzleDirective muzzleDirective,
RepositorySystem system,
RepositorySystemSession session) {
Set<MuzzleDirective> inverseDirectives = new HashSet<>();
Artifact allVersionsArtifact =
new DefaultArtifact(
muzzleDirective.getGroup().get(), muzzleDirective.getModule().get(), "jar", "[,)");
Artifact directiveArtifact =
new DefaultArtifact(
muzzleDirective.getGroup().get(),
muzzleDirective.getModule().get(),
"jar",
muzzleDirective.getVersions().get());
List<RemoteRepository> repos = getProjectRepositories(instrumentationProject);
VersionRangeRequest allRangeRequest = new VersionRangeRequest();
allRangeRequest.setRepositories(repos);
allRangeRequest.setArtifact(allVersionsArtifact);
final VersionRangeResult allRangeResult;
try {
allRangeResult = system.resolveVersionRange(session, allRangeRequest);
} catch (VersionRangeResolutionException e) {
throw new RuntimeException(e);
}
VersionRangeRequest rangeRequest = new VersionRangeRequest();
rangeRequest.setRepositories(repos);
rangeRequest.setArtifact(directiveArtifact);
final VersionRangeResult rangeResult;
try {
rangeResult = system.resolveVersionRange(session, rangeRequest);
} catch (VersionRangeResolutionException e) {
throw new RuntimeException(e);
}
allRangeResult.getVersions().removeAll(rangeResult.getVersions());
for (String version :
filterVersions(allRangeResult, muzzleDirective.getNormalizedSkipVersions())) {
MuzzleDirective inverseDirective =
instrumentationProject.getObjects().newInstance(MuzzleDirective.class);
inverseDirective.getGroup().set(muzzleDirective.getGroup());
inverseDirective.getModule().set(muzzleDirective.getModule());
inverseDirective.getVersions().set(version);
inverseDirective.getAssertPass().set(!muzzleDirective.getAssertPass().get());
inverseDirectives.add(inverseDirective);
}
return inverseDirectives;
}
private static Set<String> filterVersions(VersionRangeResult range, Set<String> skipVersions) {
Set<String> result = new HashSet<>();
AcceptableVersions predicate = new AcceptableVersions(skipVersions);
if (predicate.test(range.getLowestVersion())) {
result.add(range.getLowestVersion().toString());
}
if (predicate.test(range.getHighestVersion())) {
result.add(range.getHighestVersion().toString());
}
List<Version> copy = new ArrayList<>(range.getVersions());
Collections.shuffle(copy);
for (Version version : copy) {
if (result.size() >= RANGE_COUNT_LIMIT) {
break;
}
if (predicate.test(version)) {
result.add(version.toString());
}
}
return result;
}
/** 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, Project project) {
DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession();
File muzzleRepo = project.file("build/muzzleRepo");
LocalRepository localRepo = new LocalRepository(muzzleRepo);
session.setLocalRepositoryManager(system.newLocalRepositoryManager(session, localRepo));
return session;
}
private static int countColons(String s) {
int count = 0;
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == ':') {
count++;
}
}
return count;
}
private static void exclude(ModuleDependency dependency, String group, String module) {
Map<String, String> exclusions = new HashMap<>();
exclusions.put("group", group);
exclusions.put("module", module);
dependency.exclude(exclusions);
}
}

View File

@ -3,11 +3,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.gradle.muzzle;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.Collections;
import org.eclipse.aether.resolution.VersionRangeRequest;
import org.eclipse.aether.resolution.VersionRangeResult;
import org.eclipse.aether.version.Version;
import org.junit.jupiter.api.Test;
@ -15,9 +15,7 @@ class MuzzlePluginTest {
@Test
void rangeRequest() {
MuzzlePlugin.AcceptableVersions predicate =
new MuzzlePlugin.AcceptableVersions(
new VersionRangeResult(new VersionRangeRequest()), Collections.emptyList());
AcceptableVersions predicate = new AcceptableVersions(Collections.emptyList());
assertThat(predicate.test(new TestVersion("10.1.0-rc2+19-8e20bb26"))).isFalse();
assertThat(predicate.test(new TestVersion("2.4.5.BUILD-SNAPSHOT"))).isFalse();