Migrate MuzzlePlugin to Java (#3017)
This commit is contained in:
parent
1535834d46
commit
f3191d9e00
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
Loading…
Reference in New Issue