diff options
author | Bjørn Christian Seime <bjorncs@verizonmedia.com> | 2022-06-10 13:40:39 +0200 |
---|---|---|
committer | Bjørn Christian Seime <bjorncs@verizonmedia.com> | 2022-06-10 13:40:39 +0200 |
commit | 7f291cb8b20c0867b5ec7c5a1b5d110f9a0f0a16 (patch) | |
tree | 58b9970b0957fd4b6505928bd832b5685be763c1 /vespa-enforcer-extensions | |
parent | 57b0533a24471eff70913a069fa340a5ca1001b4 (diff) |
Add custom rule for verifying transitive Maven dependencies
Diffstat (limited to 'vespa-enforcer-extensions')
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 |