summaryrefslogtreecommitdiffstats
path: root/vespa-enforcer-extensions
diff options
context:
space:
mode:
authorBjørn Christian Seime <bjorncs@yahooinc.com>2022-11-11 13:03:08 +0100
committerBjørn Christian Seime <bjorncs@yahooinc.com>2022-11-11 13:03:16 +0100
commit56b326a1257b5f44e2da3f283c3fbcfe5fa2663a (patch)
tree4d6d37457d2fc82ae17072f110c574a2fd2fbf4b /vespa-enforcer-extensions
parent7fbe42d86205804e81fb290fdf21f5469c9f7414 (diff)
Add dependency enforcer
Compares all dependencies (including transitive) for all modules in Maven project.
Diffstat (limited to 'vespa-enforcer-extensions')
-rw-r--r--vespa-enforcer-extensions/src/main/java/com/yahoo/vespa/maven/plugin/enforcer/EnforceDependenciesAllProjects.java187
-rw-r--r--vespa-enforcer-extensions/src/test/java/com/yahoo/vespa/maven/plugin/enforcer/EnforceDependenciesAllProjectsTest.java84
-rw-r--r--vespa-enforcer-extensions/src/test/resources/allowed-dependencies.txt3
3 files changed, 274 insertions, 0 deletions
diff --git a/vespa-enforcer-extensions/src/main/java/com/yahoo/vespa/maven/plugin/enforcer/EnforceDependenciesAllProjects.java b/vespa-enforcer-extensions/src/main/java/com/yahoo/vespa/maven/plugin/enforcer/EnforceDependenciesAllProjects.java
new file mode 100644
index 00000000000..ec39bd23e3f
--- /dev/null
+++ b/vespa-enforcer-extensions/src/main/java/com/yahoo/vespa/maven/plugin/enforcer/EnforceDependenciesAllProjects.java
@@ -0,0 +1,187 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.maven.plugin.enforcer;
+
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.enforcer.rule.api.EnforcerRule;
+import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
+import org.apache.maven.enforcer.rule.api.EnforcerRuleHelper;
+import org.apache.maven.execution.MavenSession;
+import org.apache.maven.plugin.logging.Log;
+import org.apache.maven.project.DefaultProjectBuildingRequest;
+import org.apache.maven.project.MavenProject;
+import org.apache.maven.shared.dependency.graph.DependencyGraphBuilder;
+import org.apache.maven.shared.dependency.graph.DependencyGraphBuilderException;
+import org.apache.maven.shared.dependency.graph.DependencyNode;
+import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException;
+import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * @author bjorncs
+ */
+public class EnforceDependenciesAllProjects implements EnforcerRule {
+
+ private static final String WRITE_SPEC_PROP = "dependencyEnforcer.writeSpec";
+
+ private String specFile;
+ private List<String> ignored;
+
+ @Override
+ public void execute(EnforcerRuleHelper helper) throws EnforcerRuleException {
+ Log log = helper.getLog();
+ SortedSet<Dependency> dependencies = getDependenciesOfAllProjects(helper, ignored);
+ log.info("Found %d unique dependencies".formatted(dependencies.size()));
+ Path specFile = resolveSpecFile(helper, this.specFile);
+ if (System.getProperties().containsKey(WRITE_SPEC_PROP)) {
+ writeDependencySpec(specFile, dependencies);
+ log.info("Updated spec file '%s'".formatted(specFile.toString()));
+ } else {
+ validateDependencies(dependencies, specFile);
+ }
+ log.info("The dependency enforcer completed successfully");
+ }
+
+ // Config injection for rule configuration. Method names must match config XML elements.
+ @SuppressWarnings("unused") public void setSpecFile(String f) { this.specFile = f; }
+ @SuppressWarnings("unused") public String getSpecFile() { return specFile; }
+ @SuppressWarnings("unused") public void setIgnored(List<String> l) { this.ignored = l; }
+ @SuppressWarnings("unused") public List<String> getIgnored() { return ignored; }
+
+ record Dependency(String groupId, String artifactId, String version, Optional<String> classifier)
+ implements Comparable<Dependency> {
+ static Dependency fromArtifact(Artifact a) {
+ return new Dependency(
+ a.getGroupId(), a.getArtifactId(), a.getVersion(), Optional.ofNullable(a.getClassifier()));
+ }
+
+ static Dependency fromString(String s) {
+ String[] splits = s.split(":");
+ return splits.length == 3
+ ? new Dependency(splits[0], splits[1], splits[2], Optional.empty())
+ : new Dependency(splits[0], splits[1], splits[2], Optional.of(splits[3]));
+ }
+
+ String asString() {
+ var b = new StringBuilder(groupId).append(':').append(artifactId).append(':').append(version);
+ classifier.ifPresent(c -> b.append(':').append(c));
+ return b.toString();
+ }
+
+ static final Comparator<Dependency> COMPARATOR = Comparator.comparing(Dependency::groupId)
+ .thenComparing(Dependency::artifactId).thenComparing(Dependency::version)
+ .thenComparing(d -> d.classifier().orElse(""));
+ @Override public int compareTo(Dependency o) { return COMPARATOR.compare(this, o); }
+ }
+
+ static void validateDependencies(SortedSet<Dependency> dependencies, Path specFile)
+ throws EnforcerRuleException {
+ SortedSet<Dependency> allowedDependencies = loadDependencySpec(specFile);
+ SortedSet<Dependency> forbiddenDependencies = new TreeSet<>(dependencies);
+ forbiddenDependencies.removeAll(allowedDependencies);
+ SortedSet<Dependency> removeDependencies = new TreeSet<>(allowedDependencies);
+ removeDependencies.removeAll(dependencies);
+ if (!forbiddenDependencies.isEmpty() || !removeDependencies.isEmpty()) {
+ StringBuilder errorMsg = new StringBuilder("The dependency enforcer failed:\n");
+ if (!forbiddenDependencies.isEmpty()) {
+ errorMsg.append("Forbidden dependencies:\n");
+ forbiddenDependencies.forEach(d -> errorMsg.append(" - ").append(d.asString()).append('\n'));
+ }
+ if (!removeDependencies.isEmpty()) {
+ errorMsg.append("Removed dependencies:\n");
+ removeDependencies.forEach(d -> errorMsg.append(" - ").append(d.asString()).append('\n'));
+ }
+ throw new EnforcerRuleException(
+ errorMsg.append("Maven dependency validation failed. To update dependency spec run " +
+ "'mvn enforcer:enforce -D")
+ .append(WRITE_SPEC_PROP).append("'")
+ .toString());
+ }
+ }
+
+ private static SortedSet<Dependency> getDependenciesOfAllProjects(EnforcerRuleHelper helper, List<String> ignored)
+ throws EnforcerRuleException {
+ try {
+ Pattern ignorePattern = Pattern.compile(
+ ignored.stream()
+ .map(s -> s.replace(".", "\\.").replace("*", ".*").replace(":", "\\:").replace('?', '.'))
+ .collect(Collectors.joining(")|(", "^(", ")$")));
+ SortedSet<Dependency> dependencies = new TreeSet<>();
+ MavenSession session = (MavenSession) helper.evaluate("${session}");
+ var graphBuilder = helper.getComponent(DependencyGraphBuilder.class);
+ for (MavenProject project : session.getAllProjects()) {
+ var req = new DefaultProjectBuildingRequest(session.getProjectBuildingRequest());
+ req.setProject(project);
+ DependencyNode root = graphBuilder.buildDependencyGraph(req, null);
+ addDependenciesRecursive(root, dependencies, ignorePattern);
+ }
+ return dependencies;
+ } catch (ExpressionEvaluationException | DependencyGraphBuilderException | ComponentLookupException e) {
+ throw new EnforcerRuleException(e.getMessage(), e);
+ }
+ }
+
+ private static void addDependenciesRecursive(DependencyNode node, Set<Dependency> dependencies, Pattern ignored) {
+ if (node.getChildren() != null) {
+ for (DependencyNode dep : node.getChildren()) {
+ Dependency dependency = Dependency.fromArtifact(dep.getArtifact());
+ if (!dependency.version().endsWith("SNAPSHOT") && !ignored.matcher(dependency.asString()).matches()) {
+ dependencies.add(dependency);
+ }
+ addDependenciesRecursive(dep, dependencies, ignored);
+ }
+ }
+ }
+
+ private static Path resolveSpecFile(EnforcerRuleHelper helper, String specFile) throws EnforcerRuleException {
+ try {
+ MavenProject project = (MavenProject) helper.evaluate("${project}");
+ return Paths.get(project.getBasedir() + File.separator + specFile).normalize();
+ } catch (ExpressionEvaluationException e) {
+ throw new EnforcerRuleException(e.getMessage(), e);
+ }
+ }
+
+ static void writeDependencySpec(Path specFile, SortedSet<Dependency> dependencies)
+ throws EnforcerRuleException {
+ try (var out = Files.newBufferedWriter(specFile)) {
+ out.write("# Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.\n");
+ for (Dependency d : dependencies) {
+ out.write(d.asString());
+ out.write('\n');
+ }
+ } catch (IOException e) {
+ throw new EnforcerRuleException(e.getMessage(), e);
+ }
+ }
+
+ private static SortedSet<Dependency> loadDependencySpec(Path specFile) throws EnforcerRuleException {
+ try {
+ try (Stream<String> s = Files.lines(specFile)) {
+ return s.map(String::trim).filter(l -> !l.isEmpty() && !l.startsWith("#")).map(Dependency::fromString)
+ .collect(Collectors.toCollection(TreeSet::new));
+ }
+ } catch (IOException e) {
+ throw new EnforcerRuleException(e.getMessage(), e);
+ }
+ }
+
+ // Mark rule as not cachable
+ @Override public boolean isCacheable() { return false; }
+ @Override public boolean isResultValid(EnforcerRule r) { return false; }
+ @Override public String getCacheId() { return ""; }
+
+}
diff --git a/vespa-enforcer-extensions/src/test/java/com/yahoo/vespa/maven/plugin/enforcer/EnforceDependenciesAllProjectsTest.java b/vespa-enforcer-extensions/src/test/java/com/yahoo/vespa/maven/plugin/enforcer/EnforceDependenciesAllProjectsTest.java
new file mode 100644
index 00000000000..a6d6661071e
--- /dev/null
+++ b/vespa-enforcer-extensions/src/test/java/com/yahoo/vespa/maven/plugin/enforcer/EnforceDependenciesAllProjectsTest.java
@@ -0,0 +1,84 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.maven.plugin.enforcer;
+
+import com.yahoo.vespa.maven.plugin.enforcer.EnforceDependenciesAllProjects.Dependency;
+import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import static com.yahoo.vespa.maven.plugin.enforcer.EnforceDependenciesAllProjects.validateDependencies;
+import static com.yahoo.vespa.maven.plugin.enforcer.EnforceDependenciesAllProjects.writeDependencySpec;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * @author bjorncs
+ */
+class EnforceDependenciesAllProjectsTest {
+
+ @Test
+ void succeeds_dependencies_matches_spec() {
+ SortedSet<Dependency> dependencies = new TreeSet<>(Set.of(
+ Dependency.fromString("com.example:foo:1.2.3"),
+ Dependency.fromString("com.example:bar:2.3.4")));
+ Path specFile = Paths.get("src/test/resources/allowed-dependencies.txt");
+ assertDoesNotThrow(() -> validateDependencies(dependencies, specFile));
+ }
+
+ @Test
+ void fails_on_forbidden_dependency() {
+ SortedSet<Dependency> dependencies = new TreeSet<>(Set.of(
+ Dependency.fromString("com.example:foo:1.2.3"),
+ Dependency.fromString("com.example:bar:2.3.4"),
+ Dependency.fromString("com.example:foobar:3.4.5")));
+ Path specFile = Paths.get("src/test/resources/allowed-dependencies.txt");
+ var exception = assertThrows(EnforcerRuleException.class,
+ () -> validateDependencies(dependencies, specFile));
+ String expectedErrorMessage =
+ """
+ The dependency enforcer failed:
+ Forbidden dependencies:
+ - com.example:foobar:3.4.5
+ Maven dependency validation failed. To update dependency spec run 'mvn enforcer:enforce -DdependencyEnforcer.writeSpec'""";
+ assertEquals(expectedErrorMessage, exception.getMessage());
+ }
+
+ @Test
+ void fails_on_missing_dependency() {
+ SortedSet<Dependency> dependencies = new TreeSet<>(Set.of(
+ Dependency.fromString("com.example:foo:1.2.3")));
+ Path specFile = Paths.get("src/test/resources/allowed-dependencies.txt");
+ var exception = assertThrows(EnforcerRuleException.class,
+ () -> validateDependencies(dependencies, specFile));
+ String expectedErrorMessage =
+ """
+ The dependency enforcer failed:
+ Removed dependencies:
+ - com.example:bar:2.3.4
+ Maven dependency validation failed. To update dependency spec run 'mvn enforcer:enforce -DdependencyEnforcer.writeSpec'""";
+ assertEquals(expectedErrorMessage, exception.getMessage());
+ }
+
+ @Test
+ void writes_valid_spec_file(@TempDir Path tempDir) throws EnforcerRuleException, IOException {
+ SortedSet<Dependency> dependencies = new TreeSet<>(Set.of(
+ Dependency.fromString("com.example:foo:1.2.3"),
+ Dependency.fromString("com.example:bar:2.3.4")));
+ Path outputFile = tempDir.resolve("allowed-dependencies.txt");
+ writeDependencySpec(outputFile, dependencies);
+ assertEquals(
+ Files.readString(Paths.get("src/test/resources/allowed-dependencies.txt")),
+ Files.readString(outputFile));
+
+ }
+
+} \ No newline at end of file
diff --git a/vespa-enforcer-extensions/src/test/resources/allowed-dependencies.txt b/vespa-enforcer-extensions/src/test/resources/allowed-dependencies.txt
new file mode 100644
index 00000000000..dc6ab2e9be0
--- /dev/null
+++ b/vespa-enforcer-extensions/src/test/resources/allowed-dependencies.txt
@@ -0,0 +1,3 @@
+# Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+com.example:bar:2.3.4
+com.example:foo:1.2.3