summaryrefslogtreecommitdiffstats
path: root/vespa-enforcer-extensions
diff options
context:
space:
mode:
authorBjørn Christian Seime <bjorncs@verizonmedia.com>2022-06-10 13:40:39 +0200
committerBjørn Christian Seime <bjorncs@verizonmedia.com>2022-06-10 13:40:39 +0200
commit7f291cb8b20c0867b5ec7c5a1b5d110f9a0f0a16 (patch)
tree58b9970b0957fd4b6505928bd832b5685be763c1 /vespa-enforcer-extensions
parent57b0533a24471eff70913a069fa340a5ca1001b4 (diff)
Add custom rule for verifying transitive Maven dependencies
Diffstat (limited to 'vespa-enforcer-extensions')
-rw-r--r--vespa-enforcer-extensions/pom.xml56
-rw-r--r--vespa-enforcer-extensions/src/main/java/com/yahoo/vespa/maven/plugin/enforcer/DependencyEnforcer.java150
-rw-r--r--vespa-enforcer-extensions/src/test/java/com/yahoo/vespa/maven/plugin/enforcer/DependencyEnforcerTest.java73
3 files changed, 279 insertions, 0 deletions
diff --git a/vespa-enforcer-extensions/pom.xml b/vespa-enforcer-extensions/pom.xml
new file mode 100644
index 00000000000..96df2c2ac33
--- /dev/null
+++ b/vespa-enforcer-extensions/pom.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0"?>
+<!-- Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
+ http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <artifactId>vespa-enforcer-extensions</artifactId>
+ <packaging>jar</packaging>
+ <parent>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>parent</artifactId>
+ <version>8-SNAPSHOT</version>
+ <relativePath>../parent/pom.xml</relativePath>
+ </parent>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.maven.enforcer</groupId>
+ <artifactId>enforcer-api</artifactId>
+ <version>3.0.0</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.maven.shared</groupId>
+ <artifactId>maven-dependency-tree</artifactId>
+ <version>3.1.1</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.maven</groupId>
+ <artifactId>maven-core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.maven</groupId>
+ <artifactId>maven-plugin-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
diff --git a/vespa-enforcer-extensions/src/main/java/com/yahoo/vespa/maven/plugin/enforcer/DependencyEnforcer.java b/vespa-enforcer-extensions/src/main/java/com/yahoo/vespa/maven/plugin/enforcer/DependencyEnforcer.java
new file mode 100644
index 00000000000..c46a67f551f
--- /dev/null
+++ b/vespa-enforcer-extensions/src/main/java/com/yahoo/vespa/maven/plugin/enforcer/DependencyEnforcer.java
@@ -0,0 +1,150 @@
+// 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.artifact.versioning.ArtifactVersion;
+import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
+import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
+import org.apache.maven.artifact.versioning.VersionRange;
+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.project.DefaultProjectBuildingRequest;
+import org.apache.maven.project.MavenProject;
+import org.apache.maven.project.ProjectBuildingRequest;
+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.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.regex.Pattern;
+
+/**
+ * Enforces that all expected dependencies are present.
+ * Fails by default for rules that do not match any dependencies.
+ * Similar to the built-in banned-dependency rule in maven-enforcer-plugin.
+ *
+ * @author bjorncs
+ */
+public class DependencyEnforcer implements EnforcerRule {
+
+ private List<String> allowedDependencies = List.of();
+ private boolean failOnUnmatched = true;
+
+ @Override
+ public void execute(EnforcerRuleHelper helper) throws EnforcerRuleException {
+ validateDependencies(getAllDependencies(helper), Set.copyOf(allowedDependencies), failOnUnmatched);
+ }
+
+ static void validateDependencies(Set<Artifact> dependencies, Set<String> allowedRules, boolean failOnUnmatched)
+ throws EnforcerRuleException {
+ SortedSet<Artifact> unmatchedArtifacts = new TreeSet<>();
+ Set<String> matchedRules = new HashSet<>();
+ for (Artifact dependency : dependencies) {
+ boolean matches = false;
+ for (String rule : allowedRules) {
+ if (matches(dependency, rule)){
+ matchedRules.add(rule);
+ matches = true;
+ break;
+ }
+ }
+ if (!matches) {
+ unmatchedArtifacts.add(dependency);
+ }
+ }
+ SortedSet<String> unmatchedRules = new TreeSet<>(allowedRules);
+ unmatchedRules.removeAll(matchedRules);
+ if (!unmatchedArtifacts.isEmpty() || (failOnUnmatched && !unmatchedRules.isEmpty())) {
+ StringBuilder errorMessage = new StringBuilder("Vespa dependency enforcer failed:\n");
+ if (!unmatchedArtifacts.isEmpty()) {
+ errorMessage.append("Dependencies not matching any rule:\n");
+ unmatchedArtifacts.forEach(a -> errorMessage.append(" - ").append(a.toString()).append('\n'));
+ }
+ if (failOnUnmatched && !unmatchedRules.isEmpty()) {
+ errorMessage.append("Rules not matching any dependency:\n");
+ unmatchedRules.forEach(p -> errorMessage.append(" - ").append(p).append('\n'));
+ }
+ throw new EnforcerRuleException(errorMessage.toString());
+ }
+ }
+
+ private static Set<Artifact> getAllDependencies(EnforcerRuleHelper helper) throws EnforcerRuleException {
+ try {
+ MavenProject project = (MavenProject) helper.evaluate("${project}");
+ MavenSession session = (MavenSession) helper.evaluate("${session}");
+ DependencyGraphBuilder graphBuilder = helper.getComponent(DependencyGraphBuilder.class);
+ ProjectBuildingRequest buildingRequest =
+ new DefaultProjectBuildingRequest(session.getProjectBuildingRequest());
+ buildingRequest.setProject(project);
+ DependencyNode root = graphBuilder.buildDependencyGraph(buildingRequest, null);
+ return getAllRecursive(root);
+ } catch (ExpressionEvaluationException | DependencyGraphBuilderException | ComponentLookupException e) {
+ throw new EnforcerRuleException(e.getMessage(), e);
+ }
+ }
+
+ private static Set<Artifact> getAllRecursive(DependencyNode node) {
+ Set<Artifact> children = new LinkedHashSet<>();
+ if (node.getChildren() != null) {
+ for (DependencyNode dep : node.getChildren()) {
+ children.add(dep.getArtifact());
+ children.addAll(getAllRecursive(dep));
+ }
+ }
+ return children;
+ }
+
+ // Similar rule matching to bannedDependencies
+ private static boolean matches(Artifact dependency, String rule) throws EnforcerRuleException {
+ String[] segments = rule.split(":");
+ if (segments.length < 1 || segments.length > 6) throw new EnforcerRuleException("Invalid rule: " + rule);
+ if (!segmentMatches(dependency.getGroupId(), segments[0])) return false;
+ if (segments.length > 1 && !segmentMatches(dependency.getArtifactId(), segments[1])) return false;
+ if (segments.length > 2 && !versionMatches(dependency.getVersion(), segments[2])) return false;
+ if (segments.length > 3 && !segmentMatches(dependency.getType(), segments[3])) return false;
+ if (segments.length > 4 && !segmentMatches(dependency.getScope(), segments[4])) return false;
+ if (segments.length > 5 && dependency.hasClassifier() && !segmentMatches(dependency.getClassifier(), segments[5]))
+ return false;
+ return true;
+ }
+
+ private static boolean segmentMatches(String value, String segmentPattern) {
+ String regex = segmentPattern
+ .replace(".", "\\.").replace("*", ".*").replace(":", "\\:").replace('?', '.')
+ .replace("[", "\\[").replace("]", "\\]").replace("(", "\\(").replace(")", "\\)");
+ return Pattern.matches(regex, value);
+ }
+
+ private static boolean versionMatches(String rawVersion, String segmentPattern) throws EnforcerRuleException {
+ try {
+ if (segmentMatches(rawVersion, segmentPattern)) return true;
+ VersionRange allowedRange = VersionRange.createFromVersionSpec(segmentPattern);
+ ArtifactVersion version = new DefaultArtifactVersion(rawVersion);
+ ArtifactVersion recommended = allowedRange.getRecommendedVersion();
+ if (recommended == null) return allowedRange.containsVersion(version);
+ return recommended.compareTo(version) <= 0;
+ } catch (InvalidVersionSpecificationException e) {
+ throw new EnforcerRuleException(e.getMessage(), e);
+ }
+ }
+
+ public void setAllowed(List<String> allowed) { this.allowedDependencies = allowed; }
+ public List<String> getAllowed() { return allowedDependencies; }
+ public void setFailOnUnmatchedRule(boolean enabled) { this.failOnUnmatched = enabled; }
+ public boolean isFailOnUnmatchedRule() { return failOnUnmatched; }
+
+ // Mark rule as not cachable
+ @Override public boolean isCacheable() { return false; }
+ @Override public boolean isResultValid(EnforcerRule enforcerRule) { return false; }
+ @Override public String getCacheId() { return ""; }
+
+}
diff --git a/vespa-enforcer-extensions/src/test/java/com/yahoo/vespa/maven/plugin/enforcer/DependencyEnforcerTest.java b/vespa-enforcer-extensions/src/test/java/com/yahoo/vespa/maven/plugin/enforcer/DependencyEnforcerTest.java
new file mode 100644
index 00000000000..1af976ae5b0
--- /dev/null
+++ b/vespa-enforcer-extensions/src/test/java/com/yahoo/vespa/maven/plugin/enforcer/DependencyEnforcerTest.java
@@ -0,0 +1,73 @@
+package com.yahoo.vespa.maven.plugin.enforcer;// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.artifact.DefaultArtifact;
+import org.apache.maven.artifact.handler.DefaultArtifactHandler;
+import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
+import org.junit.jupiter.api.Test;
+
+import java.util.Set;
+
+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 DependencyEnforcerTest {
+
+ @Test
+ void succeeds_when_all_dependencies_and_rules_match() {
+ Set<Artifact> dependencies = Set.of(
+ artifact("com.yahoo.vespa", "container-core", "8.0.0", "provided"),
+ artifact("com.yahoo.vespa", "testutils", "8.0.0", "test"));
+ Set<String> rules = Set.of(
+ "com.yahoo.vespa:container-core:*:jar:provided",
+ "com.yahoo.vespa:*:*:jar:test");
+ assertDoesNotThrow(() -> DependencyEnforcer.validateDependencies(dependencies, rules, true));
+ }
+
+ @Test
+ void fails_on_unmatched_dependency() {
+ Set<Artifact> dependencies = Set.of(
+ artifact("com.yahoo.vespa", "container-core", "8.0.0", "provided"),
+ artifact("com.yahoo.vespa", "testutils", "8.0.0", "test"));
+ Set<String> rules = Set.of("com.yahoo.vespa:*:*:jar:test");
+ EnforcerRuleException exception = assertThrows(
+ EnforcerRuleException.class,
+ () -> DependencyEnforcer.validateDependencies(dependencies, rules, true));
+ String expectedErrorMessage =
+ """
+ Vespa dependency enforcer failed:
+ Dependencies not matching any rule:
+ - com.yahoo.vespa:container-core:jar:8.0.0:provided
+ """;
+ assertEquals(expectedErrorMessage, exception.getMessage());
+ }
+
+ @Test
+ void fails_on_unmatched_rule() {
+ Set<Artifact> dependencies = Set.of(
+ artifact("com.yahoo.vespa", "testutils", "8.0.0", "test"));
+ Set<String> rules = Set.of(
+ "com.yahoo.vespa:container-core:*:jar:provided",
+ "com.yahoo.vespa:*:*:jar:test");
+ EnforcerRuleException exception = assertThrows(
+ EnforcerRuleException.class,
+ () -> DependencyEnforcer.validateDependencies(dependencies, rules, true));
+ String expectedErrorMessage =
+ """
+ Vespa dependency enforcer failed:
+ Rules not matching any dependency:
+ - com.yahoo.vespa:container-core:*:jar:provided
+ """;
+ assertEquals(expectedErrorMessage, exception.getMessage());
+ }
+
+ private static Artifact artifact(String groupId, String artifactId, String version, String scope) {
+ return new DefaultArtifact(
+ groupId, artifactId, version, scope, "jar", /*classifier*/null, new DefaultArtifactHandler("jar"));
+ }
+
+} \ No newline at end of file