Skip to content

Commit 42e19b7

Browse files
authored
Add elasticsearch distribution plugin (#43247)
Several types of distributions are built and tested in elasticsearch, ranging from the current version, to building or downloading snapshot or released versions. Currently tests relying on these have to contain logic deciding where and how to pull down these distributions. This commit adds an distributiond download plugin for each project to manage which versions and variants the project needs. It abstracts away all need for knowing where a particular version comes from, like a local distribution or bwc project, or pulling from the elastic download service. This will be used in a followup PR by the testclusters and vagrant tests.
1 parent 3fffe41 commit 42e19b7

File tree

12 files changed

+1068
-2
lines changed

12 files changed

+1068
-2
lines changed

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,16 +109,19 @@ public BwcVersions(List<String> versionLines) {
109109
}
110110

111111
protected BwcVersions(List<String> versionLines, Version currentVersionProperty) {
112-
SortedSet<Version> allVersions = versionLines.stream()
112+
this(versionLines.stream()
113113
.map(LINE_PATTERN::matcher)
114114
.filter(Matcher::matches)
115115
.map(match -> new Version(
116116
Integer.parseInt(match.group(1)),
117117
Integer.parseInt(match.group(2)),
118118
Integer.parseInt(match.group(3))
119119
))
120-
.collect(Collectors.toCollection(TreeSet::new));
120+
.collect(Collectors.toCollection(TreeSet::new)), currentVersionProperty);
121+
}
121122

123+
// for testkit tests, until BwcVersions is extracted into an extension
124+
public BwcVersions(SortedSet<Version> allVersions, Version currentVersionProperty) {
122125
if (allVersions.isEmpty()) {
123126
throw new IllegalArgumentException("Could not parse any versions");
124127
}
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.gradle;
21+
22+
import org.elasticsearch.gradle.ElasticsearchDistribution.Flavor;
23+
import org.elasticsearch.gradle.ElasticsearchDistribution.Platform;
24+
import org.elasticsearch.gradle.ElasticsearchDistribution.Type;
25+
import org.gradle.api.GradleException;
26+
import org.gradle.api.NamedDomainObjectContainer;
27+
import org.gradle.api.Plugin;
28+
import org.gradle.api.Project;
29+
import org.gradle.api.UnknownTaskException;
30+
import org.gradle.api.artifacts.Configuration;
31+
import org.gradle.api.artifacts.ConfigurationContainer;
32+
import org.gradle.api.artifacts.Dependency;
33+
import org.gradle.api.artifacts.dsl.DependencyHandler;
34+
import org.gradle.api.artifacts.repositories.IvyArtifactRepository;
35+
import org.gradle.api.credentials.HttpHeaderCredentials;
36+
import org.gradle.api.file.FileTree;
37+
import org.gradle.api.plugins.ExtraPropertiesExtension;
38+
import org.gradle.api.tasks.Copy;
39+
import org.gradle.api.tasks.TaskProvider;
40+
import org.gradle.authentication.http.HttpHeaderAuthentication;
41+
42+
import java.io.File;
43+
import java.util.HashMap;
44+
import java.util.Locale;
45+
import java.util.Map;
46+
import java.util.concurrent.Callable;
47+
import java.util.function.Supplier;
48+
49+
/**
50+
* A plugin to manage getting and extracting distributions of Elasticsearch.
51+
*
52+
* The source of the distribution could be from a local snapshot, a locally built
53+
* bwc snapshot, or the Elastic downloads service.
54+
*/
55+
public class DistributionDownloadPlugin implements Plugin<Project> {
56+
57+
private static final String FAKE_GROUP = "elasticsearch-distribution";
58+
private static final String DOWNLOAD_REPO_NAME = "elasticsearch-downloads";
59+
60+
private BwcVersions bwcVersions;
61+
private NamedDomainObjectContainer<ElasticsearchDistribution> distributionsContainer;
62+
63+
@Override
64+
public void apply(Project project) {
65+
distributionsContainer = project.container(ElasticsearchDistribution.class, name -> new ElasticsearchDistribution(name, project));
66+
project.getExtensions().add("elasticsearch_distributions", distributionsContainer);
67+
68+
setupDownloadServiceRepo(project);
69+
70+
ExtraPropertiesExtension extraProperties = project.getExtensions().getExtraProperties();
71+
this.bwcVersions = (BwcVersions) extraProperties.get("bwcVersions");
72+
// TODO: setup snapshot dependency instead of pointing to bwc distribution projects for external projects
73+
74+
project.afterEvaluate(this::setupDistributions);
75+
}
76+
77+
// pkg private for tests
78+
void setupDistributions(Project project) {
79+
for (ElasticsearchDistribution distribution : distributionsContainer) {
80+
distribution.finalizeValues();
81+
82+
DependencyHandler dependencies = project.getDependencies();
83+
// for the distribution as a file, just depend on the artifact directly
84+
dependencies.add(distribution.configuration.getName(), dependencyNotation(project, distribution));
85+
86+
// no extraction allowed for rpm or deb
87+
if (distribution.getType() != Type.RPM && distribution.getType() != Type.DEB) {
88+
// for the distribution extracted, add a root level task that does the extraction, and depend on that
89+
// extracted configuration as an artifact consisting of the extracted distribution directory
90+
dependencies.add(distribution.getExtracted().configuration.getName(),
91+
projectDependency(project, ":", configName("extracted_elasticsearch", distribution)));
92+
// ensure a root level download task exists
93+
setupRootDownload(project.getRootProject(), distribution);
94+
}
95+
}
96+
}
97+
98+
private void setupRootDownload(Project rootProject, ElasticsearchDistribution distribution) {
99+
String extractTaskName = extractTaskName(distribution);
100+
// NOTE: this is *horrendous*, but seems to be the only way to check for the existence of a registered task
101+
try {
102+
rootProject.getTasks().named(extractTaskName);
103+
// already setup this version
104+
return;
105+
} catch (UnknownTaskException e) {
106+
// fall through: register the task
107+
}
108+
setupDownloadServiceRepo(rootProject);
109+
110+
final ConfigurationContainer configurations = rootProject.getConfigurations();
111+
String downloadConfigName = configName("elasticsearch", distribution);
112+
String extractedConfigName = "extracted_" + downloadConfigName;
113+
final Configuration downloadConfig = configurations.create(downloadConfigName);
114+
configurations.create(extractedConfigName);
115+
Object distroDep = dependencyNotation(rootProject, distribution);
116+
rootProject.getDependencies().add(downloadConfigName, distroDep);
117+
118+
// add task for extraction, delaying resolving config until runtime
119+
if (distribution.getType() == Type.ARCHIVE || distribution.getType() == Type.INTEG_TEST_ZIP) {
120+
Supplier<File> archiveGetter = downloadConfig::getSingleFile;
121+
String extractDir = rootProject.getBuildDir().toPath().resolve("elasticsearch-distros").resolve(extractedConfigName).toString();
122+
TaskProvider<Copy> extractTask = rootProject.getTasks().register(extractTaskName, Copy.class, copyTask -> {
123+
copyTask.dependsOn(downloadConfig);
124+
copyTask.doFirst(t -> rootProject.delete(extractDir));
125+
copyTask.into(extractDir);
126+
copyTask.from((Callable<FileTree>)() -> {
127+
File archiveFile = archiveGetter.get();
128+
String archivePath = archiveFile.toString();
129+
if (archivePath.endsWith(".zip")) {
130+
return rootProject.zipTree(archiveFile);
131+
} else if (archivePath.endsWith(".tar.gz")) {
132+
return rootProject.tarTree(rootProject.getResources().gzip(archiveFile));
133+
}
134+
throw new IllegalStateException("unexpected file extension on [" + archivePath + "]");
135+
});
136+
});
137+
rootProject.getArtifacts().add(extractedConfigName,
138+
rootProject.getLayout().getProjectDirectory().dir(extractDir),
139+
artifact -> artifact.builtBy(extractTask));
140+
}
141+
}
142+
143+
private static void setupDownloadServiceRepo(Project project) {
144+
if (project.getRepositories().findByName(DOWNLOAD_REPO_NAME) != null) {
145+
return;
146+
}
147+
project.getRepositories().ivy(ivyRepo -> {
148+
ivyRepo.setName(DOWNLOAD_REPO_NAME);
149+
ivyRepo.setUrl("https://artifacts.elastic.co");
150+
ivyRepo.metadataSources(IvyArtifactRepository.MetadataSources::artifact);
151+
// this header is not a credential but we hack the capability to send this header to avoid polluting our download stats
152+
ivyRepo.credentials(HttpHeaderCredentials.class, creds -> {
153+
creds.setName("X-Elastic-No-KPI");
154+
creds.setValue("1");
155+
});
156+
ivyRepo.getAuthentication().create("header", HttpHeaderAuthentication.class);
157+
ivyRepo.patternLayout(layout -> layout.artifact("/downloads/elasticsearch/[module]-[revision](-[classifier]).[ext]"));
158+
ivyRepo.content(content -> content.includeGroup(FAKE_GROUP));
159+
});
160+
project.getRepositories().all(repo -> {
161+
if (repo.getName().equals(DOWNLOAD_REPO_NAME) == false) {
162+
// all other repos should ignore the special group name
163+
repo.content(content -> content.excludeGroup(FAKE_GROUP));
164+
}
165+
});
166+
// TODO: need maven repo just for integ-test-zip, but only in external cases
167+
}
168+
169+
/**
170+
* Returns a dependency object representing the given distribution.
171+
*
172+
* The returned object is suitable to be passed to {@link DependencyHandler}.
173+
* The concrete type of the object will either be a project {@link Dependency} or
174+
* a set of maven coordinates as a {@link String}. Project dependencies point to
175+
* a project in the Elasticsearch repo either under `:distribution:bwc`,
176+
* `:distribution:archives` or :distribution:packages`. Maven coordinates point to
177+
* either the integ-test-zip coordinates on maven central, or a set of artificial
178+
* coordinates that resolve to the Elastic download service through an ivy repository.
179+
*/
180+
private Object dependencyNotation(Project project, ElasticsearchDistribution distribution) {
181+
182+
if (Version.fromString(VersionProperties.getElasticsearch()).equals(distribution.getVersion())) {
183+
return projectDependency(project, distributionProjectPath(distribution), "default");
184+
// TODO: snapshot dep when not in ES repo
185+
}
186+
BwcVersions.UnreleasedVersionInfo unreleasedInfo = bwcVersions.unreleasedInfo(distribution.getVersion());
187+
if (unreleasedInfo != null) {
188+
assert distribution.getBundledJdk();
189+
return projectDependency(project, unreleasedInfo.gradleProjectPath, distributionProjectName(distribution));
190+
}
191+
192+
if (distribution.getType() == Type.INTEG_TEST_ZIP) {
193+
return "org.elasticsearch.distribution.integ-test-zip:elasticsearch:" + distribution.getVersion();
194+
}
195+
196+
String extension = distribution.getType().toString();
197+
String classifier = "x86_64";
198+
if (distribution.getType() == Type.ARCHIVE) {
199+
extension = distribution.getPlatform() == Platform.WINDOWS ? "zip" : "tar.gz";
200+
classifier = distribution.getPlatform() + "-" + classifier;
201+
}
202+
return FAKE_GROUP + ":elasticsearch" + (distribution.getFlavor() == Flavor.OSS ? "-oss:" : ":")
203+
+ distribution.getVersion() + ":" + classifier + "@" + extension;
204+
}
205+
206+
private static Dependency projectDependency(Project project, String projectPath, String projectConfig) {
207+
208+
if (project.findProject(projectPath) == null) {
209+
throw new GradleException("no project [" + projectPath + "], project names: " + project.getRootProject().getAllprojects());
210+
}
211+
Map<String, Object> depConfig = new HashMap<>();
212+
depConfig.put("path", projectPath);
213+
depConfig.put("configuration", projectConfig);
214+
return project.getDependencies().project(depConfig);
215+
}
216+
217+
private static String distributionProjectPath(ElasticsearchDistribution distribution) {
218+
String projectPath = ":distribution";
219+
if (distribution.getType() == Type.INTEG_TEST_ZIP) {
220+
projectPath += ":archives:integ-test-zip";
221+
} else {
222+
projectPath += distribution.getType() == Type.ARCHIVE ? ":archives:" : ":packages:";
223+
projectPath += distributionProjectName(distribution);
224+
}
225+
return projectPath;
226+
}
227+
228+
private static String distributionProjectName(ElasticsearchDistribution distribution) {
229+
String projectName = "";
230+
if (distribution.getFlavor() == Flavor.OSS) {
231+
projectName += "oss-";
232+
}
233+
if (distribution.getBundledJdk() == false) {
234+
projectName += "no-jdk-";
235+
}
236+
if (distribution.getType() == Type.ARCHIVE) {
237+
Platform platform = distribution.getPlatform();
238+
projectName += platform.toString() + (platform == Platform.WINDOWS ? "-zip" : "-tar");
239+
} else {
240+
projectName += distribution.getType();
241+
}
242+
return projectName;
243+
}
244+
245+
private static String configName(String prefix, ElasticsearchDistribution distribution) {
246+
return prefix + "_" + distribution.getVersion() + "_" + distribution.getType() + "_" +
247+
(distribution.getPlatform() == null ? "" : distribution.getPlatform() + "_")
248+
+ distribution.getFlavor() + (distribution.getBundledJdk() ? "" : "_nojdk");
249+
}
250+
251+
private static String capitalize(String s) {
252+
return s.substring(0, 1).toUpperCase(Locale.ROOT) + s.substring(1);
253+
}
254+
255+
private static String extractTaskName(ElasticsearchDistribution distribution) {
256+
String taskName = "extractElasticsearch";
257+
if (distribution.getType() != Type.INTEG_TEST_ZIP) {
258+
if (distribution.getFlavor() == Flavor.OSS) {
259+
taskName += "Oss";
260+
}
261+
if (distribution.getBundledJdk() == false) {
262+
taskName += "NoJdk";
263+
}
264+
}
265+
if (distribution.getType() == Type.ARCHIVE) {
266+
taskName += capitalize(distribution.getPlatform().toString());
267+
} else if (distribution.getType() != Type.INTEG_TEST_ZIP) {
268+
taskName += capitalize(distribution.getType().toString());
269+
}
270+
taskName += distribution.getVersion();
271+
return taskName;
272+
}
273+
}

0 commit comments

Comments
 (0)