Skip to content

Commit f833218

Browse files
authored
Merge pull request #452 from DataDog/ark/version-scan-muzzle
Add version scanning to muzzle gradle plugin
2 parents 78e6f6a + 9338faa commit f833218

8 files changed

Lines changed: 372 additions & 105 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: 278 additions & 19 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.directives.any { it.assertPass }) {
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') {
@@ -43,19 +72,52 @@ class MuzzlePlugin implements Plugin<Project> {
4372
project.tasks.compileMuzzle.dependsOn(toolingProject.tasks.compileJava)
4473
project.afterEvaluate {
4574
project.tasks.compileMuzzle.dependsOn(project.tasks.compileJava)
46-
if (project.tasks.getNames().contains("compileScala")) {
75+
if (project.tasks.getNames().contains('compileScala')) {
4776
project.tasks.compileMuzzle.dependsOn(project.tasks.compileScala)
4877
}
4978
}
50-
5179
project.tasks.muzzle.dependsOn(project.tasks.compileMuzzle)
5280
project.tasks.printReferences.dependsOn(project.tasks.compileMuzzle)
81+
82+
def hasRelevantTask = project.gradle.startParameter.taskNames.any { taskName ->
83+
// removing leading ':' if present
84+
taskName = taskName.replaceFirst('^:', '')
85+
String muzzleTaskPath = project.path.replaceFirst('^:', '')
86+
return 'muzzle' == taskName || "${muzzleTaskPath}:muzzle" == taskName
87+
}
88+
if (!hasRelevantTask) {
89+
// Adding muzzle dependencies has a large config overhead. Stop unless muzzle is explicitly run.
90+
return
91+
}
92+
93+
final RepositorySystem system = newRepositorySystem()
94+
final RepositorySystemSession session = newRepositorySystemSession(system)
95+
96+
project.afterEvaluate {
97+
// use runAfter to set up task finalizers in version order
98+
Task runAfter = project.tasks.muzzle
99+
100+
for (MuzzleDirective muzzleDirective : project.muzzle.directives) {
101+
project.getLogger().info("configured ${muzzleDirective.assertPass ? 'pass' : 'fail'} directive: ${muzzleDirective.group}:${muzzleDirective.module}:${muzzleDirective.versions}")
102+
103+
muzzleDirectiveToArtifacts(muzzleDirective, system, session).collect() { Artifact singleVersion ->
104+
runAfter = addMuzzleTask(muzzleDirective, singleVersion, project, runAfter, bootstrapProject, toolingProject)
105+
}
106+
if (muzzleDirective.assertInverse) {
107+
inverseOf(muzzleDirective, system, session).collect() { MuzzleDirective inverseDirective ->
108+
muzzleDirectiveToArtifacts(inverseDirective, system, session).collect() { Artifact singleVersion ->
109+
runAfter = addMuzzleTask(inverseDirective, singleVersion, project, runAfter, bootstrapProject, toolingProject)
110+
}
111+
}
112+
}
113+
}
114+
}
53115
}
54116

55117
/**
56118
* Create a classloader with core agent classes and project instrumentation on the classpath.
57119
*/
58-
private ClassLoader createDDClassloader(Project project, Project toolingProject) {
120+
private static ClassLoader createDDClassloader(Project project, Project toolingProject) {
59121
project.getLogger().info("Creating dd classpath for: " + project.getName())
60122
Set<URL> ddUrls = new HashSet<>()
61123
for (File f : toolingProject.sourceSets.main.runtimeClasspath.getFiles()) {
@@ -71,11 +133,11 @@ class MuzzlePlugin implements Plugin<Project> {
71133
}
72134

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

0 commit comments

Comments
 (0)