Skip to content

Commit c6841c9

Browse files
author
Andrew Kent
committed
Add maven version scanning to muzzle
1 parent 78e6f6a commit c6841c9

3 files changed

Lines changed: 286 additions & 49 deletions

File tree

buildSrc/src/main/groovy/MuzzleExtension.groovy

Lines changed: 0 additions & 4 deletions
This file was deleted.

buildSrc/src/main/groovy/MuzzlePlugin.groovy

Lines changed: 237 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,61 @@
1+
import org.apache.maven.repository.internal.MavenRepositorySystemUtils
2+
import org.eclipse.aether.DefaultRepositorySystemSession
3+
import org.eclipse.aether.RepositorySystem
4+
import org.eclipse.aether.RepositorySystemSession
5+
import org.eclipse.aether.artifact.Artifact
6+
import org.eclipse.aether.artifact.DefaultArtifact
7+
import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory
8+
import org.eclipse.aether.impl.DefaultServiceLocator
9+
import org.eclipse.aether.repository.LocalRepository
10+
import org.eclipse.aether.repository.RemoteRepository
11+
import org.eclipse.aether.resolution.VersionRangeRequest
12+
import org.eclipse.aether.resolution.VersionRangeResult
13+
import org.eclipse.aether.spi.connector.RepositoryConnectorFactory
14+
import org.eclipse.aether.spi.connector.transport.TransporterFactory
15+
import org.eclipse.aether.transport.http.HttpTransporterFactory
16+
import org.eclipse.aether.version.Version
17+
import org.gradle.api.Action
118
import org.gradle.api.Plugin
219
import org.gradle.api.Project
20+
import org.gradle.api.Task
21+
import org.gradle.api.model.ObjectFactory
322

423
import java.lang.reflect.Method
524

625
/**
7-
* muzzle task plugin which runs muzzle validation against an instrumentation's compile-time dependencies.
8-
*
9-
* <p/>TODO: merge this with version scan
26+
* muzzle task plugin which runs muzzle validation against a range of dependencies.
1027
*/
1128
class MuzzlePlugin implements Plugin<Project> {
29+
/**
30+
* Remote repositories used to query version ranges and fetch dependencies
31+
*/
32+
private static final List<RemoteRepository> MUZZLE_REPOS
33+
static {
34+
RemoteRepository central = new RemoteRepository.Builder("central", "default", "http://central.maven.org/maven2/").build()
35+
MUZZLE_REPOS = new ArrayList<RemoteRepository>(Arrays.asList(central))
36+
}
37+
1238
@Override
1339
void apply(Project project) {
1440
def bootstrapProject = project.rootProject.getChildProjects().get('dd-java-agent').getChildProjects().get('agent-bootstrap')
1541
def toolingProject = project.rootProject.getChildProjects().get('dd-java-agent').getChildProjects().get('agent-tooling')
16-
project.extensions.create("muzzle", MuzzleExtension)
17-
def compileMuzzle = project.task('compileMuzzle') {
18-
// not adding user and group to hide this from `gradle tasks`
19-
}
42+
project.extensions.create("muzzle", MuzzleExtension, project.objects)
43+
44+
// compileMuzzle compiles all projects required to run muzzle validation.
45+
// Not adding group and description to keep this task from showing in `gradle tasks`.
46+
def compileMuzzle = project.task('compileMuzzle')
2047
def muzzle = project.task('muzzle') {
2148
group = 'Muzzle'
2249
description = "Run instrumentation muzzle on compile time dependencies"
2350
doLast {
24-
final ClassLoader userCL = createUserClassLoader(project, bootstrapProject)
25-
final ClassLoader agentCL = createDDClassloader(project, toolingProject)
26-
// find all instrumenters, get muzzle, and assert
27-
Method assertionMethod = agentCL.loadClass('datadog.trace.agent.tooling.muzzle.MuzzleVersionScanPlugin')
28-
.getMethod('assertInstrumentationNotMuzzled', ClassLoader.class)
29-
assertionMethod.invoke(null, userCL)
51+
if (project.muzzle.passDirectives.size() == 0) {
52+
project.getLogger().info('No muzzle pass directives configured. Asserting pass against instrumentation compile-time dependencies')
53+
final ClassLoader userCL = createCompileDepsClassLoader(project, bootstrapProject)
54+
final ClassLoader agentCL = createDDClassloader(project, toolingProject)
55+
Method assertionMethod = agentCL.loadClass('datadog.trace.agent.tooling.muzzle.MuzzleVersionScanPlugin')
56+
.getMethod('assertInstrumentationMuzzled', ClassLoader.class, boolean.class)
57+
assertionMethod.invoke(null, userCL, true)
58+
}
3059
}
3160
}
3261
def printReferences = project.task('printReferences') {
@@ -47,15 +76,52 @@ class MuzzlePlugin implements Plugin<Project> {
4776
project.tasks.compileMuzzle.dependsOn(project.tasks.compileScala)
4877
}
4978
}
50-
79+
// TODO: consider:
80+
// project.tasks.withType(ScalaCompile) { Task scalaTask ->
81+
// project.tasks.compileMuzzle.dependsOn(scalaTask)
82+
// }
5183
project.tasks.muzzle.dependsOn(project.tasks.compileMuzzle)
5284
project.tasks.printReferences.dependsOn(project.tasks.compileMuzzle)
85+
86+
def hasRelevantTask = project.gradle.startParameter.taskNames.any { taskName ->
87+
// removing leading ':' if present
88+
taskName = taskName.replaceFirst('^:', '')
89+
String muzzleTaskPath = project.path.replaceFirst('^:', '')
90+
return 'muzzle' == taskName || "${muzzleTaskPath}:muzzle" == taskName
91+
}
92+
if (!hasRelevantTask) {
93+
// Adding muzzle dependencies has a large config overhead. Stop unless muzzle is explicitly run.
94+
return
95+
}
96+
97+
final RepositorySystem system = newRepositorySystem()
98+
final RepositorySystemSession session = newRepositorySystemSession(system)
99+
100+
project.afterEvaluate {
101+
// use runAfter to set up task finalizers in version order
102+
Task runAfter = project.tasks.muzzle
103+
104+
for (MuzzleDirective pass : project.muzzle.passDirectives) {
105+
project.getLogger().info("configured pass directive: ${pass.group}:${pass.module}:${pass.versions}")
106+
107+
muzzleDirectiveToArtifacts(pass, system, session).collect() { Artifact singleVersion ->
108+
runAfter = addMuzzleTask(true, singleVersion, project, runAfter, bootstrapProject, toolingProject)
109+
}
110+
}
111+
for (MuzzleDirective fail : project.muzzle.failDirectives) {
112+
project.getLogger().info("configured fail directive: ${fail.group}:${fail.module}:${fail.versions}")
113+
114+
muzzleDirectiveToArtifacts(fail, system, session).collect() { Artifact singleVersion ->
115+
runAfter = addMuzzleTask(false, singleVersion, project, runAfter, bootstrapProject, toolingProject)
116+
}
117+
}
118+
}
53119
}
54120

55121
/**
56122
* Create a classloader with core agent classes and project instrumentation on the classpath.
57123
*/
58-
private ClassLoader createDDClassloader(Project project, Project toolingProject) {
124+
private static ClassLoader createDDClassloader(Project project, Project toolingProject) {
59125
project.getLogger().info("Creating dd classpath for: " + project.getName())
60126
Set<URL> ddUrls = new HashSet<>()
61127
for (File f : toolingProject.sourceSets.main.runtimeClasspath.getFiles()) {
@@ -71,11 +137,11 @@ class MuzzlePlugin implements Plugin<Project> {
71137
}
72138

73139
/**
74-
* Create a classloader with user/library classes on the classpath.
140+
* Create a classloader with all compile-time dependencies on the classpath
75141
*/
76-
private ClassLoader createUserClassLoader(Project project, Project bootstrapProject) {
142+
private static ClassLoader createCompileDepsClassLoader(Project project, Project bootstrapProject) {
77143
List<URL> userUrls = new ArrayList<>()
78-
project.getLogger().info("Creating user classpath for: " + project.getName())
144+
project.getLogger().info("Creating compile-time classpath for: " + project.getName())
79145
for (File f : project.configurations.compileOnly.getFiles()) {
80146
project.getLogger().info('--' + f)
81147
userUrls.add(f.toURI().toURL())
@@ -86,4 +152,157 @@ class MuzzlePlugin implements Plugin<Project> {
86152
}
87153
return new URLClassLoader(userUrls.toArray(new URL[0]), (ClassLoader) null)
88154
}
155+
156+
/**
157+
* Create a classloader with dependencies for a single muzzle task.
158+
*/
159+
private static ClassLoader createClassLoaderForTask(Project project, Project bootstrapProject, String muzzleTaskName) {
160+
final List<URL> userUrls = new ArrayList<>()
161+
162+
project.getLogger().info("Creating task classpath")
163+
project.configurations.getByName(muzzleTaskName).resolvedConfiguration.files.each { File jarFile ->
164+
project.getLogger().info("-- Added to instrumentation classpath: $jarFile")
165+
userUrls.add(jarFile.toURI().toURL())
166+
}
167+
168+
for (File f : bootstrapProject.sourceSets.main.runtimeClasspath.getFiles()) {
169+
project.getLogger().info("-- Added to instrumentation bootstrap classpath: $f")
170+
userUrls.add(f.toURI().toURL())
171+
}
172+
return new URLClassLoader(userUrls.toArray(new URL[0]), (ClassLoader) null)
173+
}
174+
175+
/**
176+
* Convert a muzzle directive to a list of artifacts
177+
*/
178+
private static List<Artifact> muzzleDirectiveToArtifacts(MuzzleDirective muzzleDirective, RepositorySystem system, RepositorySystemSession session) {
179+
final Artifact directiveArtifact = new DefaultArtifact(muzzleDirective.group, muzzleDirective.module, "jar", muzzleDirective.versions)
180+
181+
final VersionRangeRequest rangeRequest = new VersionRangeRequest()
182+
rangeRequest.setRepositories(MUZZLE_REPOS)
183+
rangeRequest.setArtifact(directiveArtifact)
184+
final VersionRangeResult rangeResult = system.resolveVersionRange(session, rangeRequest)
185+
186+
final List<Artifact> allVersionArtifacts = filterVersion(rangeResult.versions).collect { version ->
187+
new DefaultArtifact(muzzleDirective.group, muzzleDirective.module, "jar", version.toString())
188+
}
189+
190+
return allVersionArtifacts
191+
}
192+
193+
/**
194+
* Configure a muzzle task to pass or fail a given version.
195+
*
196+
* @param assertPass If true, assert that muzzle validation passes
197+
* @param versionArtifact version to assert against.
198+
* @param instrumentationProject instrumentation being asserted against.
199+
* @param runAfter Task which runs before the new muzzle task.
200+
* @param bootstrapProject Agent bootstrap project.
201+
* @param toolingProject Agent tooling project.
202+
*
203+
* @return The created muzzle task.
204+
*/
205+
private static Task addMuzzleTask(boolean assertPass, Artifact versionArtifact, Project instrumentationProject, Task runAfter, Project bootstrapProject, Project toolingProject) {
206+
def taskName = "muzzle-Assert${assertPass ? "Pass" : "Fail"}-$versionArtifact.groupId-$versionArtifact.artifactId-$versionArtifact.version"
207+
def config = instrumentationProject.configurations.create(taskName)
208+
config.dependencies.add(instrumentationProject.dependencies.create("$versionArtifact.groupId:$versionArtifact.artifactId:$versionArtifact.version") {
209+
transitive = true
210+
})
211+
212+
def muzzleTask = instrumentationProject.task(taskName) {
213+
doLast {
214+
final ClassLoader userCL = createClassLoaderForTask(instrumentationProject, bootstrapProject, taskName)
215+
final ClassLoader agentCL = createDDClassloader(instrumentationProject, toolingProject)
216+
// find all instrumenters, get muzzle, and assert
217+
Method assertionMethod = agentCL.loadClass('datadog.trace.agent.tooling.muzzle.MuzzleVersionScanPlugin')
218+
.getMethod('assertInstrumentationMuzzled', ClassLoader.class, boolean.class)
219+
assertionMethod.invoke(null, userCL, assertPass)
220+
}
221+
}
222+
runAfter.finalizedBy(muzzleTask)
223+
return muzzleTask
224+
}
225+
226+
/**
227+
* Create muzzle's repository system
228+
*/
229+
private static RepositorySystem newRepositorySystem() {
230+
DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator()
231+
locator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class)
232+
locator.addService(TransporterFactory.class, HttpTransporterFactory.class)
233+
234+
return locator.getService(RepositorySystem.class)
235+
}
236+
237+
/**
238+
* Create muzzle's repository system session
239+
*/
240+
private static RepositorySystemSession newRepositorySystemSession(RepositorySystem system) {
241+
DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession()
242+
243+
def tempDir = File.createTempDir()
244+
tempDir.deleteOnExit()
245+
LocalRepository localRepo = new LocalRepository(tempDir)
246+
session.setLocalRepositoryManager(system.newLocalRepositoryManager(session, localRepo))
247+
248+
return session
249+
}
250+
251+
/**
252+
* Filter out snapshot-type builds from versions list.
253+
*/
254+
private static filterVersion(List<Version> list) {
255+
list.removeIf {
256+
def version = it.toString().toLowerCase()
257+
return version.contains("rc") ||
258+
version.contains(".cr") ||
259+
version.contains("alpha") ||
260+
version.contains("beta") ||
261+
version.contains("-b") ||
262+
version.contains(".m") ||
263+
version.contains("-dev") ||
264+
version.contains("public_draft")
265+
}
266+
return list
267+
}
268+
}
269+
270+
// plugin extension classes
271+
272+
/**
273+
* A pass or fail directive for a single dependency.
274+
*/
275+
class MuzzleDirective {
276+
String group
277+
String module
278+
String versions
279+
}
280+
281+
/**
282+
* Muzzle extension containing all pass and fail directives.
283+
*/
284+
class MuzzleExtension {
285+
// TODO: merge pass and fail directives into single collection
286+
final List<MuzzleDirective> passDirectives
287+
final List<MuzzleDirective> failDirectives
288+
private final ObjectFactory objectFactory
289+
290+
@javax.inject.Inject
291+
MuzzleExtension(final ObjectFactory objectFactory) {
292+
this.objectFactory = objectFactory
293+
passDirectives = new ArrayList<>()
294+
failDirectives = new ArrayList<>()
295+
}
296+
297+
void pass(Action<? super MuzzleDirective> action) {
298+
final MuzzleDirective pass = objectFactory.newInstance(MuzzleDirective)
299+
action.execute(pass)
300+
passDirectives.add(pass)
301+
}
302+
303+
void fail(Action<? super MuzzleDirective> action) {
304+
final MuzzleDirective fail = objectFactory.newInstance(MuzzleDirective)
305+
action.execute(fail)
306+
failDirectives.add(fail)
307+
}
89308
}

0 commit comments

Comments
 (0)