Skip to content

Commit 5c5e98b

Browse files
authored
Fix ElasticsearchNode input calculation to be Gradle 7.0 compliant (#70943) (#71458)
This makes test cluster input calculation Gradle 7.0 compliant. It reworks input tracking for test cluster by using artifact transforms to inspect plugin zips in ElasticseachNode effectivly. By using artifact transforms we also ensure we do not unpack unchanged modules or plugins again and again and reuse former unpacked archives instead across multiple builds. We also add full integ test coverage for ElasticsearchNode inputs calculation logic here.
1 parent 88b3d65 commit 5c5e98b

File tree

9 files changed

+220
-33
lines changed

9 files changed

+220
-33
lines changed

buildSrc/src/integTest/groovy/org/elasticsearch/gradle/TestClustersPluginFuncTest.groovy

Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@
99
package org.elasticsearch.gradle
1010

1111
import org.elasticsearch.gradle.fixtures.AbstractGradleFuncTest
12+
import org.gradle.testkit.runner.GradleRunner
1213
import spock.lang.IgnoreIf
14+
import spock.lang.Unroll
1315

16+
import static org.elasticsearch.gradle.fixtures.DistributionDownloadFixture.withChangedClasspathMockedDistributionDownload
17+
import static org.elasticsearch.gradle.fixtures.DistributionDownloadFixture.withChangedConfigMockedDistributionDownload
1418
import static org.elasticsearch.gradle.fixtures.DistributionDownloadFixture.withMockedDistributionDownload
1519

1620
/**
@@ -22,14 +26,34 @@ class TestClustersPluginFuncTest extends AbstractGradleFuncTest {
2226

2327
def setup() {
2428
buildFile << """
25-
import org.elasticsearch.gradle.testclusters.DefaultTestClustersTask
29+
import org.elasticsearch.gradle.testclusters.TestClustersAware
30+
import org.elasticsearch.gradle.testclusters.ElasticsearchCluster
2631
plugins {
2732
id 'elasticsearch.testclusters'
2833
}
2934
30-
class SomeClusterAwareTask extends DefaultTestClustersTask {
35+
class SomeClusterAwareTask extends DefaultTask implements TestClustersAware {
36+
37+
private Collection<ElasticsearchCluster> clusters = new HashSet<>();
38+
39+
@Override
40+
@Nested
41+
public Collection<ElasticsearchCluster> getClusters() {
42+
return clusters;
43+
}
44+
45+
@OutputFile
46+
Provider<RegularFile> outputFile
47+
48+
@Inject
49+
SomeClusterAwareTask(ProjectLayout projectLayout) {
50+
outputFile = projectLayout.getBuildDirectory().file("someclusteraware.txt")
51+
}
52+
3153
@TaskAction void doSomething() {
54+
outputFile.get().getAsFile().text = "done"
3255
println 'SomeClusterAwareTask executed'
56+
3357
}
3458
}
3559
"""
@@ -61,6 +85,98 @@ class TestClustersPluginFuncTest extends AbstractGradleFuncTest {
6185
assertNoCustomDistro('myCluster')
6286
}
6387

88+
@Unroll
89+
def "test cluster #inputProperty change is detected"() {
90+
given:
91+
buildFile << """
92+
testClusters {
93+
myCluster {
94+
testDistribution = 'default'
95+
}
96+
}
97+
98+
tasks.register('myTask', SomeClusterAwareTask) {
99+
useCluster testClusters.myCluster
100+
}
101+
"""
102+
103+
when:
104+
def runner = gradleRunner("myTask", '-i', '-g', 'guh')
105+
def runningClosure = { GradleRunner r -> r.build() }
106+
withMockedDistributionDownload(runner, runningClosure)
107+
def result = inputProperty == "distributionClasspath" ?
108+
withChangedClasspathMockedDistributionDownload(runner, runningClosure) :
109+
withChangedConfigMockedDistributionDownload(runner, runningClosure)
110+
111+
then:
112+
normalized(result.output).contains("Task ':myTask' is not up-to-date because:\n Input property 'clusters.myCluster\$0.nodes.\$0.$inputProperty'")
113+
result.output.contains("elasticsearch-keystore script executed!")
114+
assertEsLogContains("myCluster", "Starting Elasticsearch process")
115+
assertEsLogContains("myCluster", "Stopping node")
116+
assertNoCustomDistro('myCluster')
117+
118+
where:
119+
inputProperty << ["distributionClasspath", "distributionFiles"]
120+
}
121+
122+
@Unroll
123+
def "test cluster modules #propertyName change is detected"() {
124+
given:
125+
addSubProject("test-module") << """
126+
plugins {
127+
id 'elasticsearch.esplugin'
128+
}
129+
// do not hassle with resolving predefined dependencies
130+
configurations.compileOnly.dependencies.clear()
131+
configurations.testImplementation.dependencies.clear()
132+
133+
esplugin {
134+
name = 'test-module'
135+
classname 'org.acme.TestModule'
136+
description = "test module description"
137+
}
138+
139+
licenseFile = file('license.txt')
140+
noticeFile = file('notice.txt')
141+
version = "1.0"
142+
group = 'org.acme'
143+
"""
144+
buildFile << """
145+
testClusters {
146+
myCluster {
147+
testDistribution = 'default'
148+
module ':test-module'
149+
}
150+
}
151+
152+
tasks.register('myTask', SomeClusterAwareTask) {
153+
useCluster testClusters.myCluster
154+
}
155+
"""
156+
157+
when:
158+
withMockedDistributionDownload(gradleRunner("myTask", '-g', 'guh')) {
159+
build()
160+
}
161+
fileChange.delegate = this
162+
fileChange.call(this)
163+
def result = withMockedDistributionDownload(gradleRunner("myTask", '-i', '-g', 'guh')) {
164+
build()
165+
}
166+
167+
then:
168+
normalized(result.output).contains("Task ':myTask' is not up-to-date because:\n" +
169+
" Input property 'clusters.myCluster\$0.nodes.\$0.$propertyName'")
170+
result.output.contains("elasticsearch-keystore script executed!")
171+
assertEsLogContains("myCluster", "Starting Elasticsearch process")
172+
assertEsLogContains("myCluster", "Stopping node")
173+
174+
where:
175+
propertyName | fileChange
176+
"installedFiles" | { def testClazz -> testClazz.file("test-module/src/main/plugin-metadata/someAddedConfig.txt") << "new resource file" }
177+
"installedClasspath" | { def testClazz -> testClazz.file("test-module/src/main/java/SomeClass.java") << "class SomeClass {}" }
178+
}
179+
64180
def "can declare test cluster in lazy evaluated task configuration block"() {
65181
given:
66182
buildFile << """

buildSrc/src/integTest/groovy/org/elasticsearch/gradle/fixtures/DistributionDownloadFixture.groovy

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,30 @@ class DistributionDownloadFixture {
2323
gradleRunner, buildRunClosure)
2424
}
2525

26+
static BuildResult withChangedClasspathMockedDistributionDownload(GradleRunner gradleRunner, Closure<BuildResult> buildRunClosure) {
27+
return doRunWithMockedDistributionDownload(VersionProperties.getElasticsearch(), ElasticsearchDistribution.CURRENT_PLATFORM,
28+
gradleRunner, buildRunClosure, true, false)
29+
}
30+
static BuildResult withChangedConfigMockedDistributionDownload(GradleRunner gradleRunner, Closure<BuildResult> buildRunClosure) {
31+
return doRunWithMockedDistributionDownload(VersionProperties.getElasticsearch(), ElasticsearchDistribution.CURRENT_PLATFORM,
32+
gradleRunner, buildRunClosure, false, true)
33+
}
34+
2635
static BuildResult withMockedDistributionDownload(String version, ElasticsearchDistribution.Platform platform,
2736
GradleRunner gradleRunner, Closure<BuildResult> buildRunClosure) {
37+
return doRunWithMockedDistributionDownload(version, platform, gradleRunner, buildRunClosure, false, false)
38+
}
39+
40+
static BuildResult withChangedClasspathMockedDistributionDownload(String version, ElasticsearchDistribution.Platform platform,
41+
GradleRunner gradleRunner, Closure<BuildResult> buildRunClosure) {
42+
return doRunWithMockedDistributionDownload(version, platform, gradleRunner, buildRunClosure, true, false)
43+
}
44+
45+
private static BuildResult doRunWithMockedDistributionDownload(String version, ElasticsearchDistribution.Platform platform,
46+
GradleRunner gradleRunner, Closure<BuildResult> buildRunClosure,
47+
boolean withAddedJar, boolean withAddedConfig) {
2848
String urlPath = urlPath(version, platform);
29-
return WiremockFixture.withWireMock(urlPath, filebytes(urlPath)) { server ->
49+
return WiremockFixture.withWireMock(urlPath, filebytes(urlPath, withAddedJar, withAddedConfig)) { server ->
3050
File initFile = new File(gradleRunner.getProjectDir(), INIT_SCRIPT)
3151
initFile.text = """allprojects { p ->
3252
p.repositories.all { repo ->
@@ -47,8 +67,9 @@ class DistributionDownloadFixture {
4767
"/downloads/elasticsearch/elasticsearch-${version}-${platform}-${Architecture.current().classifier}.$fileType"
4868
}
4969

50-
private static byte[] filebytes(String urlPath) throws IOException {
51-
String suffix = urlPath.endsWith("zip") ? "zip" : "tar.gz";
52-
return DistributionDownloadFixture.getResourceAsStream("/org/elasticsearch/gradle/fake_elasticsearch." + suffix).getBytes()
70+
private static byte[] filebytes(String urlPath, boolean withAddedJar, boolean withAddedConfig) throws IOException {
71+
String distro = (withAddedJar ? "-with-added-jar" : "") + (withAddedConfig ? "-with-added-config" : "")
72+
String suffix = urlPath.endsWith("zip") ? ".zip" : ".tar.gz";
73+
return DistributionDownloadFixture.getResourceAsStream("/org/elasticsearch/gradle/fake_elasticsearch${distro}" + suffix).getBytes()
5374
}
5475
}

buildSrc/src/main/java/org/elasticsearch/gradle/RepositoriesSetupPlugin.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public static void configureRepositories(Project project) {
8282
}
8383

8484
private static void assertRepositoryURIIsSecure(final String repositoryName, final String projectPath, final URI uri) {
85-
if (uri != null && SECURE_URL_SCHEMES.contains(uri.getScheme()) == false) {
85+
if (uri != null && SECURE_URL_SCHEMES.contains(uri.getScheme()) == false && uri.getHost().equals("localhost") == false) {
8686
String url;
8787
try {
8888
url = uri.toURL().toString();

buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/ElasticsearchNode.java

Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,23 @@
2323
import org.elasticsearch.gradle.VersionProperties;
2424
import org.elasticsearch.gradle.http.WaitForHttpResource;
2525
import org.elasticsearch.gradle.info.BuildParams;
26+
import org.elasticsearch.gradle.transform.UnzipTransform;
2627
import org.elasticsearch.gradle.util.Pair;
2728
import org.gradle.api.Action;
2829
import org.gradle.api.Named;
2930
import org.gradle.api.NamedDomainObjectContainer;
3031
import org.gradle.api.Project;
3132
import org.gradle.api.artifacts.Configuration;
33+
import org.gradle.api.artifacts.Dependency;
34+
import org.gradle.api.artifacts.type.ArtifactTypeDefinition;
35+
import org.gradle.api.attributes.Attribute;
3236
import org.gradle.api.file.ArchiveOperations;
37+
import org.gradle.api.file.ConfigurableFileCollection;
38+
import org.gradle.api.file.FileCollection;
3339
import org.gradle.api.file.FileSystemOperations;
3440
import org.gradle.api.file.FileTree;
3541
import org.gradle.api.file.RegularFile;
42+
import org.gradle.api.internal.artifacts.ArtifactAttributes;
3643
import org.gradle.api.logging.Logger;
3744
import org.gradle.api.logging.Logging;
3845
import org.gradle.api.provider.Provider;
@@ -64,7 +71,6 @@
6471
import java.util.Arrays;
6572
import java.util.Collection;
6673
import java.util.Collections;
67-
import java.util.Comparator;
6874
import java.util.HashMap;
6975
import java.util.HashSet;
7076
import java.util.LinkedHashMap;
@@ -126,6 +132,7 @@ public class ElasticsearchNode implements TestClusterConfiguration {
126132

127133
private final LinkedHashMap<String, Predicate<TestClusterConfiguration>> waitConditions = new LinkedHashMap<>();
128134
private final Map<String, Configuration> pluginAndModuleConfigurations = new HashMap<>();
135+
private final ConfigurableFileCollection pluginAndModuleConfiguration;
129136
private final List<Provider<File>> plugins = new ArrayList<>();
130137
private final List<Provider<File>> modules = new ArrayList<>();
131138
final LazyPropertyMap<String, CharSequence> settings = new LazyPropertyMap<>("Settings", this);
@@ -161,6 +168,8 @@ public class ElasticsearchNode implements TestClusterConfiguration {
161168
private String keystorePassword = "";
162169
private boolean preserveDataDir = false;
163170

171+
private Attribute<Boolean> bundleAttribute = Attribute.of("bundle", Boolean.class);
172+
164173
ElasticsearchNode(
165174
String clusterName,
166175
String path,
@@ -195,8 +204,10 @@ public class ElasticsearchNode implements TestClusterConfiguration {
195204
waitConditions.put("ports files", this::checkPortsFilesExistWithDelay);
196205
defaultConfig.put("cluster.name", clusterName);
197206

207+
pluginAndModuleConfiguration = project.getObjects().fileCollection();
198208
setTestDistribution(TestDistribution.INTEG_TEST);
199209
setVersion(VersionProperties.getElasticsearch());
210+
configureArtifactTransforms();
200211
}
201212

202213
@Input
@@ -281,11 +292,15 @@ Collection<Configuration> getPluginAndModuleConfigurations() {
281292
// creates a configuration to depend on the given plugin project, then wraps that configuration
282293
// to grab the zip as a file provider
283294
private Provider<RegularFile> maybeCreatePluginOrModuleDependency(String path) {
284-
Configuration configuration = pluginAndModuleConfigurations.computeIfAbsent(
285-
path,
286-
key -> project.getConfigurations()
287-
.detachedConfiguration(project.getDependencies().project(Map.of("path", path, "configuration", "zip")))
288-
);
295+
Configuration configuration = pluginAndModuleConfigurations.computeIfAbsent(path, key -> {
296+
Dependency bundleDependency = this.project.getDependencies().project(Map.of("path", path, "configuration", "zip"));
297+
return project.getConfigurations().detachedConfiguration(bundleDependency);
298+
});
299+
300+
/**
301+
* dependencies.create(files(provider { println("provider"); File(".") }))
302+
*
303+
* */
289304
Provider<File> fileProvider = configuration.getElements()
290305
.map(
291306
s -> s.stream()
@@ -299,6 +314,7 @@ private Provider<RegularFile> maybeCreatePluginOrModuleDependency(String path) {
299314
@Override
300315
public void plugin(Provider<RegularFile> plugin) {
301316
checkFrozen();
317+
registerExtractedConfig(plugin);
302318
this.plugins.add(plugin.map(RegularFile::getAsFile));
303319
}
304320

@@ -310,9 +326,32 @@ public void plugin(String pluginProjectPath) {
310326
@Override
311327
public void module(Provider<RegularFile> module) {
312328
checkFrozen();
329+
registerExtractedConfig(module);
313330
this.modules.add(module.map(RegularFile::getAsFile));
314331
}
315332

333+
private void registerExtractedConfig(Provider<RegularFile> pluginProvider) {
334+
Dependency pluginDependency = this.project.getDependencies().create(project.files(pluginProvider));
335+
Configuration extractedConfig = project.getConfigurations().detachedConfiguration(pluginDependency);
336+
extractedConfig.getAttributes().attribute(ArtifactAttributes.ARTIFACT_FORMAT, ArtifactTypeDefinition.DIRECTORY_TYPE);
337+
extractedConfig.getAttributes().attribute(bundleAttribute, true);
338+
pluginAndModuleConfiguration.from(extractedConfig);
339+
}
340+
341+
private void configureArtifactTransforms() {
342+
project.getDependencies().getAttributesSchema().attribute(bundleAttribute);
343+
project.getDependencies().getArtifactTypes().maybeCreate(ArtifactTypeDefinition.ZIP_TYPE);
344+
project.getDependencies().registerTransform(UnzipTransform.class, transformSpec -> {
345+
transformSpec.getFrom()
346+
.attribute(ArtifactAttributes.ARTIFACT_FORMAT, ArtifactTypeDefinition.ZIP_TYPE)
347+
.attribute(bundleAttribute, true);
348+
transformSpec.getTo()
349+
.attribute(ArtifactAttributes.ARTIFACT_FORMAT, ArtifactTypeDefinition.DIRECTORY_TYPE)
350+
.attribute(bundleAttribute, true);
351+
transformSpec.getParameters().setAsFiletreeOutput(true);
352+
});
353+
}
354+
316355
@Override
317356
public void module(String moduleProjectPath) {
318357
module(maybeCreatePluginOrModuleDependency(moduleProjectPath));
@@ -1325,26 +1364,15 @@ private Path getExtractedDistributionDir() {
13251364
return distributions.get(currentDistro).getExtracted().getSingleFile().toPath();
13261365
}
13271366

1328-
private List<File> getInstalledFileSet(Action<? super PatternFilterable> filter) {
1329-
return Stream.concat(plugins.stream().map(Provider::get), modules.stream().map(Provider::get))
1330-
.filter(File::exists)
1331-
// TODO: We may be able to simplify this with Gradle 5.6
1332-
// https://docs.gradle.org/nightly/release-notes.html#improved-handling-of-zip-archives-on-classpaths
1333-
.map(zipFile -> archiveOperations.zipTree(zipFile).matching(filter))
1334-
.flatMap(tree -> tree.getFiles().stream())
1335-
.sorted(Comparator.comparing(File::getName))
1336-
.collect(Collectors.toList());
1337-
}
1338-
13391367
@Classpath
1340-
public List<File> getInstalledClasspath() {
1341-
return getInstalledFileSet(filter -> filter.include("**/*.jar"));
1368+
public FileCollection getInstalledClasspath() {
1369+
return pluginAndModuleConfiguration.filter(f -> f.isDirectory() == false && f.getName().endsWith(".jar"));
13421370
}
13431371

13441372
@InputFiles
13451373
@PathSensitive(PathSensitivity.RELATIVE)
1346-
public List<File> getInstalledFiles() {
1347-
return getInstalledFileSet(filter -> filter.exclude("**/*.jar"));
1374+
public FileCollection getInstalledFiles() {
1375+
return pluginAndModuleConfiguration.filter(f -> f.isDirectory() == false && f.getName().endsWith(".jar") == false);
13481376
}
13491377

13501378
@Classpath

buildSrc/src/main/java/org/elasticsearch/gradle/transform/SymbolicLinkPreservingUntarTransform.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
1212
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
1313
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
14+
import org.gradle.api.artifacts.transform.TransformOutputs;
1415

1516
import java.io.File;
1617
import java.io.FileInputStream;
@@ -27,7 +28,7 @@ public abstract class SymbolicLinkPreservingUntarTransform implements UnpackTran
2728

2829
private static final Path CURRENT_DIR_PATH = Paths.get(".");
2930

30-
public void unpack(File tarFile, File targetDir) throws IOException {
31+
public void unpack(File tarFile, File targetDir, TransformOutputs outputs, boolean asFiletreeOutput) throws IOException {
3132
Function<String, Path> pathModifier = pathResolver();
3233
TarArchiveInputStream tar = new TarArchiveInputStream(new GzipCompressorInputStream(new FileInputStream(tarFile)));
3334
final Path destinationPath = targetDir.toPath();
@@ -54,6 +55,9 @@ public void unpack(File tarFile, File targetDir) throws IOException {
5455
try (FileOutputStream fos = new FileOutputStream(destination.toFile())) {
5556
tar.transferTo(fos);
5657
}
58+
if (asFiletreeOutput) {
59+
outputs.file(destination.toFile());
60+
}
5761
}
5862
if (entry.isSymbolicLink() == false) {
5963
// check if the underlying file system supports POSIX permissions

0 commit comments

Comments
 (0)