Просмотр исходного кода

Convert RunTask to use testclusers, remove ClusterFormationTasks (#47572)

* Convert RunTask to use testclusers, remove ClusterFormationTasks

This PR adds a new RunTask and a way for it to start a
testclusters cluster out of band and block on it to replace
the old RunTask that used ClusterFormationTasks.

With this we can now remove ClusterFormationTasks.
Alpar Torok 6 лет назад
Родитель
Сommit
8f7855464a

+ 5 - 5
build.gradle

@@ -17,17 +17,17 @@
  * under the License.
  */
 
+
 import com.github.jengelman.gradle.plugins.shadow.ShadowPlugin
 import org.apache.tools.ant.taskdefs.condition.Os
 import org.elasticsearch.gradle.BuildPlugin
-import org.elasticsearch.gradle.Version
 import org.elasticsearch.gradle.BwcVersions
+import org.elasticsearch.gradle.Version
 import org.elasticsearch.gradle.VersionProperties
 import org.elasticsearch.gradle.plugin.PluginBuildPlugin
-import org.elasticsearch.gradle.tool.Boilerplate
-import org.gradle.util.GradleVersion
-import org.gradle.util.DistributionLocator
 import org.gradle.plugins.ide.eclipse.model.SourceFolder
+import org.gradle.util.DistributionLocator
+import org.gradle.util.GradleVersion
 
 import static org.elasticsearch.gradle.tool.Boilerplate.maybeConfigure
 
@@ -449,7 +449,7 @@ class Run extends DefaultTask {
         description = "Enable debugging configuration, to allow attaching a debugger to elasticsearch."
   )
   public void setDebug(boolean enabled) {
-    project.project(':distribution').run.clusterConfig.debug = enabled
+    project.project(':distribution').run.debug = enabled
   }
 }
 task run(type: Run) {

+ 23 - 38
buildSrc/src/main/groovy/org/elasticsearch/gradle/plugin/PluginBuildPlugin.groovy

@@ -24,7 +24,7 @@ import org.elasticsearch.gradle.NoticeTask
 import org.elasticsearch.gradle.Version
 import org.elasticsearch.gradle.VersionProperties
 import org.elasticsearch.gradle.test.RestIntegTestTask
-import org.elasticsearch.gradle.test.RunTask
+import org.elasticsearch.gradle.testclusters.RunTask
 import org.elasticsearch.gradle.testclusters.TestClustersPlugin
 import org.elasticsearch.gradle.tool.ClasspathUtils
 import org.gradle.api.InvalidUserDataException
@@ -65,35 +65,28 @@ class PluginBuildPlugin implements Plugin<Project> {
             project.archivesBaseName = name
             project.description = extension1.description
             configurePublishing(project, extension1)
-            if (project.plugins.hasPlugin(TestClustersPlugin.class) == false) {
-                project.integTestCluster.dependsOn(project.tasks.bundlePlugin)
-                if (isModule) {
-                    project.integTestCluster.module(project)
-                } else {
-                    project.integTestCluster.plugin(project.path)
-                }
+
+            project.tasks.integTest.dependsOn(project.tasks.bundlePlugin)
+            if (isModule) {
+                project.testClusters.integTest.module(
+                        project.file(project.tasks.bundlePlugin.archiveFile)
+                )
             } else {
-                project.tasks.integTest.dependsOn(project.tasks.bundlePlugin)
-                if (isModule) {
+                project.testClusters.integTest.plugin(
+                        project.file(project.tasks.bundlePlugin.archiveFile)
+                )
+            }
+
+            project.extensions.getByType(PluginPropertiesExtension).extendedPlugins.each { pluginName ->
+                // Auto add dependent modules to the test cluster
+                if (project.findProject(":modules:${pluginName}") != null) {
+                    project.integTest.dependsOn(project.project(":modules:${pluginName}").tasks.bundlePlugin)
                     project.testClusters.integTest.module(
-                            project.file(project.tasks.bundlePlugin.archiveFile)
-                    )
-                } else {
-                    project.testClusters.integTest.plugin(
-                            project.file(project.tasks.bundlePlugin.archiveFile)
+                            project.file(project.project(":modules:${pluginName}").tasks.bundlePlugin.archiveFile)
                     )
                 }
-
-                project.extensions.getByType(PluginPropertiesExtension).extendedPlugins.each { pluginName ->
-                    // Auto add dependent modules to the test cluster
-                    if (project.findProject(":modules:${pluginName}") != null) {
-                        project.integTest.dependsOn(project.project(":modules:${pluginName}").tasks.bundlePlugin)
-                        project.testClusters.integTest.module(
-                                project.file(project.project(":modules:${pluginName}").tasks.bundlePlugin.archiveFile)
-                        )
-                    }
-                }
             }
+
             if (extension1.name == null) {
                 throw new InvalidUserDataException('name is a required setting for esplugin')
             }
@@ -117,14 +110,6 @@ class PluginBuildPlugin implements Plugin<Project> {
             ]
             buildProperties.expand(properties)
             buildProperties.inputs.properties(properties)
-            project.tasks.run.dependsOn(project.tasks.bundlePlugin)
-            if (isModule) {
-                project.tasks.run.clusterConfig.distribution = System.getProperty(
-                        'run.distribution', isXPackModule ? 'default' : 'oss'
-                )
-            } else {
-                project.tasks.run.clusterConfig.plugin(project.path)
-            }
             if (isModule == false || isXPackModule) {
                 addNoticeGeneration(project, extension1)
             }
@@ -145,7 +130,11 @@ class PluginBuildPlugin implements Plugin<Project> {
         createIntegTestTask(project)
         createBundleTasks(project, extension)
         project.configurations.getByName('default').extendsFrom(project.configurations.getByName('runtime'))
-        project.tasks.create('run', RunTask) // allow running ES with this plugin in the foreground of a build
+        // allow running ES with this plugin in the foreground of a build
+        project.tasks.register('run', RunTask) {
+            dependsOn(project.tasks.bundlePlugin)
+            useCluster project.testClusters.integTest
+        }
     }
 
 
@@ -178,10 +167,6 @@ class PluginBuildPlugin implements Plugin<Project> {
     private static void createIntegTestTask(Project project) {
         RestIntegTestTask integTest = project.tasks.create('integTest', RestIntegTestTask.class)
         integTest.mustRunAfter('precommit', 'test')
-        if (project.plugins.hasPlugin(TestClustersPlugin.class) == false) {
-            // only if not using test clusters
-            project.integTestCluster.distribution = System.getProperty('tests.distribution', 'integ-test-zip')
-        }
         project.check.dependsOn(integTest)
     }
 

+ 0 - 254
buildSrc/src/main/groovy/org/elasticsearch/gradle/test/ClusterConfiguration.groovy

@@ -1,254 +0,0 @@
-/*
- * Licensed to Elasticsearch under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.elasticsearch.gradle.test
-
-import org.elasticsearch.gradle.Version
-import org.gradle.api.GradleException
-import org.gradle.api.Project
-import org.gradle.api.tasks.Input
-
-/** Configuration for an elasticsearch cluster, used for integration tests. */
-class ClusterConfiguration {
-
-    private final Project project
-
-    @Input
-    String distribution = 'default'
-
-    @Input
-    int numNodes = 1
-
-    @Input
-    int numBwcNodes = 0
-
-    @Input
-    Version bwcVersion = null
-
-    @Input
-    int httpPort = 0
-
-    @Input
-    int transportPort = 0
-
-    /**
-     * An override of the data directory. Input is the node number and output
-     * is the override data directory.
-     */
-    @Input
-    Closure<String> dataDir = null
-
-    /** Optional override of the cluster name. */
-    @Input
-    String clusterName = null
-
-    @Input
-    boolean daemonize = true
-
-    @Input
-    boolean debug = false
-
-    /**
-     * Whether the initial_master_nodes setting should be automatically derived from the nodes
-     * in the cluster. Only takes effect if all nodes in the cluster understand this setting
-     * and the discovery type is not explicitly set.
-     */
-    @Input
-    boolean autoSetInitialMasterNodes = true
-
-    /**
-     * Whether the file-based discovery provider should be automatically setup based on
-     * the nodes in the cluster. Only takes effect if no other hosts provider is already
-     * configured.
-     */
-    @Input
-    boolean autoSetHostsProvider = true
-
-    @Input
-    String jvmArgs = "-Xms" + System.getProperty('tests.heap.size', '512m') +
-        " " + "-Xmx" + System.getProperty('tests.heap.size', '512m') +
-        " " + System.getProperty('tests.jvm.argline', '')
-
-    /**
-     * Should the shared environment be cleaned on cluster startup? Defaults
-     * to {@code true} so we run with a clean cluster but some tests wish to
-     * preserve snapshots between clusters so they set this to true.
-     */
-    @Input
-    boolean cleanShared = true
-
-    /**
-     * A closure to call which returns the unicast host to connect to for cluster formation.
-     *
-     * This allows multi node clusters, or a new cluster to connect to an existing cluster.
-     * The closure takes three arguments, the NodeInfo for the first node in the cluster,
-     * the NodeInfo for the node current being configured, an AntBuilder which may be used
-     * to wait on conditions before returning.
-     */
-    @Input
-    Closure unicastTransportUri = { NodeInfo seedNode, NodeInfo node, AntBuilder ant ->
-        if (seedNode == node) {
-            return null
-        }
-        ant.waitfor(maxwait: '40', maxwaitunit: 'second', checkevery: '500', checkeveryunit: 'millisecond',
-                timeoutproperty: "failed.${seedNode.transportPortsFile.path}") {
-            resourceexists {
-                file(file: seedNode.transportPortsFile.toString())
-            }
-        }
-        if (ant.properties.containsKey("failed.${seedNode.transportPortsFile.path}".toString())) {
-            throw new GradleException("Failed to locate seed node transport file [${seedNode.transportPortsFile}]: " +
-                    "timed out waiting for it to be created after 40 seconds")
-        }
-        return seedNode.transportUri()
-    }
-
-    /**
-     * A closure to call which returns a manually supplied list of unicast seed hosts.
-     */
-    @Input
-    Closure<List<String>> otherUnicastHostAddresses = {
-        Collections.emptyList()
-    }
-
-    /**
-     * A closure to call before the cluster is considered ready. The closure is passed the node info,
-     * as well as a groovy AntBuilder, to enable running ant condition checks. The default wait
-     * condition is for http on the http port.
-     */
-    @Input
-    Closure waitCondition = { NodeInfo node, AntBuilder ant ->
-        File tmpFile = new File(node.cwd, 'wait.success')
-        String waitUrl = "http://${node.httpUri()}/_cluster/health?wait_for_nodes=>=${numNodes}&wait_for_status=yellow"
-        ant.echo(message: "==> [${new Date()}] checking health: ${waitUrl}",
-                 level: 'info')
-        // checking here for wait_for_nodes to be >= the number of nodes because its possible
-        // this cluster is attempting to connect to nodes created by another task (same cluster name),
-        // so there will be more nodes in that case in the cluster state
-        ant.get(src: waitUrl,
-                dest: tmpFile.toString(),
-                ignoreerrors: true, // do not fail on error, so logging buffers can be flushed by the wait task
-                retries: 10)
-        return tmpFile.exists()
-    }
-
-    /**
-     * The maximum number of seconds to wait for nodes to complete startup, which includes writing
-     * the ports files for the transports and the pid file. This wait time occurs before the wait
-     * condition is executed.
-     */
-    @Input
-    int nodeStartupWaitSeconds = 30
-
-    public ClusterConfiguration(Project project) {
-        this.project = project
-    }
-
-    // **Note** for systemProperties, settings, keystoreFiles etc:
-    // value could be a GString that is evaluated to just a String
-    // there are cases when value depends on task that is not executed yet on configuration stage
-    Map<String, Object> systemProperties = new HashMap<>()
-
-    Map<String, Object> environmentVariables = new HashMap<>()
-
-    Map<String, Object> settings = new HashMap<>()
-
-    Map<String, String> keystoreSettings = new HashMap<>()
-
-    Map<String, Object> keystoreFiles = new HashMap<>()
-
-    // map from destination path, to source file
-    Map<String, Object> extraConfigFiles = new HashMap<>()
-
-    LinkedHashMap<String, Object> plugins = new LinkedHashMap<>()
-
-    List<Project> modules = new ArrayList<>()
-
-    LinkedHashMap<String, Object[]> setupCommands = new LinkedHashMap<>()
-
-    List<Object> dependencies = new ArrayList<>()
-
-    @Input
-    void systemProperty(String property, Object value) {
-        systemProperties.put(property, value)
-    }
-
-    @Input
-    void environment(String variable, Object value) {
-        environmentVariables.put(variable, value)
-    }
-
-    @Input
-    void setting(String name, Object value) {
-        settings.put(name, value)
-    }
-
-    @Input
-    void keystoreSetting(String name, String value) {
-        keystoreSettings.put(name, value)
-    }
-
-    /**
-     * Adds a file to the keystore. The name is the secure setting name, and the sourceFile
-     * is anything accepted by project.file()
-     */
-    @Input
-    void keystoreFile(String name, Object sourceFile) {
-        keystoreFiles.put(name, sourceFile)
-    }
-
-    @Input
-    void plugin(String path) {
-        Project pluginProject = project.project(path)
-        plugins.put(pluginProject.name, pluginProject)
-    }
-
-    @Input
-    void mavenPlugin(String name, String mavenCoords) {
-        plugins.put(name, mavenCoords)
-    }
-
-    /** Add a module to the cluster. The project must be an esplugin and have a single zip default artifact. */
-    @Input
-    void module(Project moduleProject) {
-        modules.add(moduleProject)
-    }
-
-    @Input
-    void setupCommand(String name, Object... args) {
-        setupCommands.put(name, args)
-    }
-
-    /**
-     * Add an extra configuration file. The path is relative to the config dir, and the sourceFile
-     * is anything accepted by project.file()
-     */
-    @Input
-    void extraConfigFile(String path, Object sourceFile) {
-        if (path == 'elasticsearch.yml') {
-            throw new GradleException('Overwriting elasticsearch.yml is not allowed, add additional settings using cluster { setting "foo", "bar" }')
-        }
-        extraConfigFiles.put(path, sourceFile)
-    }
-
-    /** Add dependencies that must be run before the first task setting up the cluster. */
-    @Input
-    void dependsOn(Object... deps) {
-        dependencies.addAll(deps)
-    }
-}

+ 0 - 991
buildSrc/src/main/groovy/org/elasticsearch/gradle/test/ClusterFormationTasks.groovy

@@ -1,991 +0,0 @@
-/*
- * Licensed to Elasticsearch under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.elasticsearch.gradle.test
-
-import org.apache.tools.ant.DefaultLogger
-import org.apache.tools.ant.taskdefs.condition.Os
-import org.elasticsearch.gradle.BuildPlugin
-import org.elasticsearch.gradle.BwcVersions
-import org.elasticsearch.gradle.LoggedExec
-import org.elasticsearch.gradle.Version
-import org.elasticsearch.gradle.VersionProperties
-import org.elasticsearch.gradle.plugin.PluginBuildPlugin
-import org.elasticsearch.gradle.plugin.PluginPropertiesExtension
-import org.gradle.api.AntBuilder
-import org.gradle.api.DefaultTask
-import org.gradle.api.GradleException
-import org.gradle.api.Project
-import org.gradle.api.Task
-import org.gradle.api.artifacts.Configuration
-import org.gradle.api.artifacts.Dependency
-import org.gradle.api.file.FileCollection
-import org.gradle.api.logging.Logger
-import org.gradle.api.tasks.Copy
-import org.gradle.api.tasks.Delete
-import org.gradle.api.tasks.Exec
-import org.gradle.internal.jvm.Jvm
-
-import java.nio.charset.StandardCharsets
-import java.nio.file.Paths
-import java.util.concurrent.TimeUnit
-import java.util.stream.Collectors
-
-/**
- * A helper for creating tasks to build a cluster that is used by a task, and tear down the cluster when the task is finished.
- */
-class ClusterFormationTasks {
-
-    /**
-     * Adds dependent tasks to the given task to start and stop a cluster with the given configuration.
-     *
-     * Returns a list of NodeInfo objects for each node in the cluster.
-     */
-    static List<NodeInfo> setup(Project project, String prefix, Task runner, ClusterConfiguration config) {
-        File sharedDir = new File(project.buildDir, "cluster/shared")
-        Object startDependencies = config.dependencies
-        /* First, if we want a clean environment, we remove everything in the
-         * shared cluster directory to ensure there are no leftovers in repos
-         * or anything in theory this should not be necessary but repositories
-         * are only deleted in the cluster-state and not on-disk such that
-         * snapshots survive failures / test runs and there is no simple way
-         * today to fix that. */
-        if (config.cleanShared) {
-          Task cleanup = project.tasks.create(
-            name: "${prefix}#prepareCluster.cleanShared",
-            type: Delete,
-            dependsOn: startDependencies) {
-              delete sharedDir
-              doLast {
-                  sharedDir.mkdirs()
-              }
-          }
-          startDependencies = cleanup
-        }
-        List<Task> startTasks = []
-        List<NodeInfo> nodes = []
-        if (config.numNodes < config.numBwcNodes) {
-            throw new GradleException("numNodes must be >= numBwcNodes [${config.numNodes} < ${config.numBwcNodes}]")
-        }
-        if (config.numBwcNodes > 0 && config.bwcVersion == null) {
-            throw new GradleException("bwcVersion must not be null if numBwcNodes is > 0")
-        }
-        // this is our current version distribution configuration we use for all kinds of REST tests etc.
-        Configuration currentDistro = project.configurations.create("${prefix}_elasticsearchDistro")
-        Configuration bwcDistro = project.configurations.create("${prefix}_elasticsearchBwcDistro")
-        Configuration bwcPlugins = project.configurations.create("${prefix}_elasticsearchBwcPlugins")
-        if (System.getProperty('tests.distribution', 'oss') == 'integ-test-zip') {
-            throw new Exception("tests.distribution=integ-test-zip is not supported")
-        }
-        configureDistributionDependency(project, config.distribution, currentDistro, VersionProperties.elasticsearch)
-        boolean hasBwcNodes = config.numBwcNodes > 0
-        if (hasBwcNodes) {
-            if (config.bwcVersion == null) {
-                throw new IllegalArgumentException("Must specify bwcVersion when numBwcNodes > 0")
-            }
-            // if we have a cluster that has a BWC cluster we also need to configure a dependency on the BWC version
-            // this version uses the same distribution etc. and only differs in the version we depend on.
-            // from here on everything else works the same as if it's the current version, we fetch the BWC version
-            // from mirrors using gradles built-in mechanism etc.
-
-            configureDistributionDependency(project, config.distribution, bwcDistro, config.bwcVersion.toString())
-            for (Map.Entry<String, Object> entry : config.plugins.entrySet()) {
-                configureBwcPluginDependency(project, entry.getValue(), bwcPlugins, config.bwcVersion)
-            }
-            bwcDistro.resolutionStrategy.cacheChangingModulesFor(0, TimeUnit.SECONDS)
-            bwcPlugins.resolutionStrategy.cacheChangingModulesFor(0, TimeUnit.SECONDS)
-        }
-        for (int i = 0; i < config.numNodes; i++) {
-            // we start N nodes and out of these N nodes there might be M bwc nodes.
-            // for each of those nodes we might have a different configuration
-            Configuration distro
-            String elasticsearchVersion
-            if (i < config.numBwcNodes) {
-                elasticsearchVersion = config.bwcVersion.toString()
-                if (project.bwcVersions.unreleased.contains(config.bwcVersion) && 
-                    (project.version != elasticsearchVersion)) {
-                    elasticsearchVersion += "-SNAPSHOT"
-                }
-                distro = bwcDistro
-            } else {
-                elasticsearchVersion = VersionProperties.elasticsearch
-                distro = currentDistro
-            }
-            NodeInfo node = new NodeInfo(config, i, project, prefix, elasticsearchVersion, sharedDir)
-            nodes.add(node)
-            Closure<Map> writeConfigSetup
-            Object dependsOn
-            writeConfigSetup = { Map esConfig ->
-                if (config.getAutoSetHostsProvider()) {
-                    if (esConfig.containsKey("discovery.seed_providers") == false) {
-                        esConfig["discovery.seed_providers"] = 'file'
-                    }
-                    esConfig["discovery.seed_hosts"] = []
-                }
-                if (esConfig['discovery.type'] == null && config.getAutoSetInitialMasterNodes()) {
-                    esConfig['cluster.initial_master_nodes'] = nodes.stream().map({ n ->
-                        if (n.config.settings['node.name'] == null) {
-                            return "node-" + n.nodeNum
-                        } else {
-                            return n.config.settings['node.name']
-                        }
-                    }).collect(Collectors.toList())
-                }
-                esConfig
-            }
-            dependsOn = startDependencies
-            startTasks.add(configureNode(project, prefix, runner, dependsOn, node, config, distro, writeConfigSetup))
-        }
-
-        Task wait = configureWaitTask("${prefix}#wait", project, nodes, startTasks, config.nodeStartupWaitSeconds)
-        runner.dependsOn(wait)
-
-        return nodes
-    }
-
-    /** Adds a dependency on the given distribution */
-    static void configureDistributionDependency(Project project, String distro, Configuration configuration, String elasticsearchVersion) {
-        boolean internalBuild = project.hasProperty('bwcVersions')
-        if (distro.equals("integ-test-zip")) {
-            // short circuit integ test so it doesn't complicate the rest of the distribution setup below
-            if (internalBuild) {
-                project.dependencies.add(
-                        configuration.name,
-                        project.dependencies.project(path: ":distribution", configuration: 'integ-test-zip')
-                )
-            } else {
-                project.dependencies.add(
-                        configuration.name,
-                        "org.elasticsearch.distribution.integ-test-zip:elasticsearch:${elasticsearchVersion}@zip"
-                )
-            }
-            return
-        }
-        // TEMP HACK
-        // The oss docs CI build overrides the distro on the command line. This hack handles backcompat until CI is updated.
-        if (distro.equals('oss-zip')) {
-            distro = 'oss'
-        }
-        if (distro.equals('zip')) {
-            distro = 'default'
-        }
-        // END TEMP HACK
-        if (['oss', 'default'].contains(distro) == false) {
-            throw new GradleException("Unknown distribution: ${distro} in project ${project.path}")
-        }
-        Version version = Version.fromString(elasticsearchVersion)
-        String os = getOs()
-        String classifier = "${os}-x86_64"
-        String packaging = os.equals('windows') ? 'zip' : 'tar.gz'
-        String artifactName = 'elasticsearch'
-        if (distro.equals('oss') && Version.fromString(elasticsearchVersion).onOrAfter('6.3.0')) {
-            artifactName += '-oss'
-        }
-        Object dependency
-        String snapshotProject = "${os}-${os.equals('windows') ? 'zip' : 'tar'}"
-        if (version.before("7.0.0")) {
-            snapshotProject = "zip"
-        }
-        if (distro.equals("oss")) {
-            snapshotProject = "oss-" + snapshotProject
-        }
-        
-        BwcVersions.UnreleasedVersionInfo unreleasedInfo = null
-
-        if (project.hasProperty('bwcVersions')) {
-            // NOTE: leniency is needed for external plugin authors using build-tools. maybe build the version compat info into build-tools?
-            unreleasedInfo = project.bwcVersions.unreleasedInfo(version)
-        }
-        if (unreleasedInfo != null) {
-            dependency = project.dependencies.project(
-                    path: unreleasedInfo.gradleProjectPath, configuration: snapshotProject
-            )
-        } else if (internalBuild && elasticsearchVersion.equals(VersionProperties.elasticsearch)) {
-            dependency = project.dependencies.project(path: ":distribution:archives:${snapshotProject}")
-        } else {
-            if (version.before('7.0.0')) {
-                classifier = "" // for bwc, before we had classifiers
-            }
-            // group does not matter as it is not used when we pull from the ivy repo that points to the download service
-            dependency = "dnm:${artifactName}:${elasticsearchVersion}-${classifier}@${packaging}"
-        }
-        project.dependencies.add(configuration.name, dependency)
-    }
-
-    /** Adds a dependency on a different version of the given plugin, which will be retrieved using gradle's dependency resolution */
-    static void configureBwcPluginDependency(Project project, Object plugin, Configuration configuration, Version elasticsearchVersion) {
-        if (plugin instanceof Project) {
-            Project pluginProject = (Project)plugin
-            verifyProjectHasBuildPlugin(configuration.name, elasticsearchVersion, project, pluginProject)
-            final String pluginName = findPluginName(pluginProject)
-            project.dependencies.add(configuration.name, "org.elasticsearch.plugin:${pluginName}:${elasticsearchVersion}@zip")
-        } else {
-            project.dependencies.add(configuration.name, "${plugin}@zip")
-        }
-    }
-
-    /**
-     * Adds dependent tasks to start an elasticsearch cluster before the given task is executed,
-     * and stop it after it has finished executing.
-     *
-     * The setup of the cluster involves the following:
-     * <ol>
-     *   <li>Cleanup the extraction directory</li>
-     *   <li>Extract a fresh copy of elasticsearch</li>
-     *   <li>Write an elasticsearch.yml config file</li>
-     *   <li>Copy plugins that will be installed to a temporary dir (which contains spaces)</li>
-     *   <li>Install plugins</li>
-     *   <li>Run additional setup commands</li>
-     *   <li>Start elasticsearch<li>
-     * </ol>
-     *
-     * @return a task which starts the node.
-     */
-    static Task configureNode(Project project, String prefix, Task runner, Object dependsOn, NodeInfo node, ClusterConfiguration config,
-                              Configuration distribution, Closure<Map> writeConfig) {
-
-        // tasks are chained so their execution order is maintained
-        Task setup = project.tasks.create(name: taskName(prefix, node, 'clean'), type: Delete, dependsOn: dependsOn) {
-            delete node.homeDir
-            delete node.cwd
-        }
-        setup = project.tasks.create(name: taskName(prefix, node, 'createCwd'), type: DefaultTask, dependsOn: setup) {
-            doLast {
-                node.cwd.mkdirs()
-            }
-            outputs.dir node.cwd
-        }
-        setup = configureCheckPreviousTask(taskName(prefix, node, 'checkPrevious'), project, setup, node)
-        setup = configureStopTask(taskName(prefix, node, 'stopPrevious'), project, setup, node)
-        setup = configureExtractTask(taskName(prefix, node, 'extract'), project, setup, node, distribution, config.distribution)
-        setup = configureWriteConfigTask(taskName(prefix, node, 'configure'), project, setup, node, writeConfig)
-        setup = configureCreateKeystoreTask(taskName(prefix, node, 'createKeystore'), project, setup, node)
-        setup = configureAddKeystoreSettingTasks(prefix, project, setup, node)
-        setup = configureAddKeystoreFileTasks(prefix, project, setup, node)
-
-        if (node.config.plugins.isEmpty() == false) {
-            if (node.nodeVersion == Version.fromString(VersionProperties.elasticsearch)) {
-                setup = configureCopyPluginsTask(taskName(prefix, node, 'copyPlugins'), project, setup, node, prefix)
-            } else {
-                setup = configureCopyBwcPluginsTask(taskName(prefix, node, 'copyBwcPlugins'), project, setup, node, prefix)
-            }
-        }
-
-        // install modules
-        for (Project module : node.config.modules) {
-            String actionName = pluginTaskName('install', module.name, 'Module')
-            setup = configureInstallModuleTask(taskName(prefix, node, actionName), project, setup, node, module)
-        }
-
-        // install plugins
-        for (String pluginName : node.config.plugins.keySet()) {
-            String actionName = pluginTaskName('install', pluginName, 'Plugin')
-            setup = configureInstallPluginTask(taskName(prefix, node, actionName), project, setup, node, pluginName, prefix)
-        }
-
-        // sets up any extra config files that need to be copied over to the ES instance;
-        // its run after plugins have been installed, as the extra config files may belong to plugins
-        setup = configureExtraConfigFilesTask(taskName(prefix, node, 'extraConfig'), project, setup, node)
-
-        // extra setup commands
-        for (Map.Entry<String, Object[]> command : node.config.setupCommands.entrySet()) {
-            // the first argument is the actual script name, relative to home
-            Object[] args = command.getValue().clone()
-            final Object commandPath
-            if (Os.isFamily(Os.FAMILY_WINDOWS)) {
-                /*
-                 * We have to delay building the string as the path will not exist during configuration which will fail on Windows due to
-                 * getting the short name requiring the path to already exist. Note that we have to capture the value of arg[0] now
-                 * otherwise we would stack overflow later since arg[0] is replaced below.
-                 */
-                String argsZero = args[0]
-                commandPath = "${-> Paths.get(NodeInfo.getShortPathName(node.homeDir.toString())).resolve(argsZero.toString()).toString()}"
-            } else {
-                commandPath = node.homeDir.toPath().resolve(args[0].toString()).toString()
-            }
-            args[0] = commandPath
-            setup = configureExecTask(taskName(prefix, node, command.getKey()), project, setup, node, args)
-        }
-
-        Task start = configureStartTask(taskName(prefix, node, 'start'), project, setup, node)
-
-        if (node.config.daemonize) {
-            Task stop = configureStopTask(taskName(prefix, node, 'stop'), project, [], node)
-            // if we are running in the background, make sure to stop the server when the task completes
-            runner.finalizedBy(stop)
-            start.finalizedBy(stop)
-            for (Object dependency : config.dependencies) {
-                if (dependency instanceof Fixture) {
-                    def depStop = ((Fixture)dependency).stopTask
-                    runner.finalizedBy(depStop)
-                    start.finalizedBy(depStop)
-                }
-            }
-        }
-        return start
-    }
-
-    /** Adds a task to extract the elasticsearch distribution */
-    static Task configureExtractTask(String name, Project project, Task setup, NodeInfo node,
-                                     Configuration configuration, String distribution) {
-        List extractDependsOn = [configuration, setup]
-        /* configuration.singleFile will be an external artifact if this is being run by a plugin not living in the
-          elasticsearch source tree. If this is a plugin built in the elasticsearch source tree or this is a distro in
-          the elasticsearch source tree then this should be the version of elasticsearch built by the source tree.
-          If it isn't then Bad Things(TM) will happen. */
-        Task extract = project.tasks.create(name: name, type: Copy, dependsOn: extractDependsOn) {
-            if (getOs().equals("windows") || distribution.equals("integ-test-zip")) {
-                from {
-                    project.zipTree(configuration.singleFile)
-                }
-            } else {
-                // macos and linux use tar
-                from {
-                    project.tarTree(project.resources.gzip(configuration.singleFile))
-                }
-            }
-            into node.baseDir
-        }
-
-        return extract
-    }
-
-    /** Adds a task to write elasticsearch.yml for the given node configuration */
-    static Task configureWriteConfigTask(String name, Project project, Task setup, NodeInfo node, Closure<Map> configFilter) {
-        Map esConfig = [
-                'cluster.name'                 : node.clusterName,
-                'node.name'                    : "node-" + node.nodeNum,
-                (node.nodeVersion.onOrAfter('7.4.0') ? 'node.pidfile' : 'pidfile') : node.pidFile,
-                'path.repo'                    : "${node.sharedDir}/repo",
-                'path.shared_data'             : "${node.sharedDir}/",
-                // Define a node attribute so we can test that it exists
-                'node.attr.testattr'           : 'test',
-                // Don't wait for state, just start up quickly. This will also allow new and old nodes in the BWC case to become the master
-                'discovery.initial_state_timeout' : '0s'
-        ]
-        esConfig['http.port'] = node.config.httpPort
-        if (node.nodeVersion.onOrAfter('6.7.0')) {
-            esConfig['transport.port'] =  node.config.transportPort
-        } else {
-            esConfig['transport.tcp.port'] =  node.config.transportPort
-        }
-        // Default the watermarks to absurdly low to prevent the tests from failing on nodes without enough disk space
-        esConfig['cluster.routing.allocation.disk.watermark.low'] = '1b'
-        esConfig['cluster.routing.allocation.disk.watermark.high'] = '1b'
-        if (node.nodeVersion.major >= 6) {
-            esConfig['cluster.routing.allocation.disk.watermark.flood_stage'] = '1b'
-        }
-        // increase script compilation limit since tests can rapid-fire script compilations
-        esConfig['script.max_compilations_rate'] = '2048/1m'
-        // Temporarily disable the real memory usage circuit breaker. It depends on real memory usage which we have no full control
-        // over and the REST client will not retry on circuit breaking exceptions yet (see #31986 for details). Once the REST client
-        // can retry on circuit breaking exceptions, we can revert again to the default configuration.
-        if (node.nodeVersion.major >= 7) {
-            esConfig['indices.breaker.total.use_real_memory'] = false
-        }
-
-        Task writeConfig = project.tasks.create(name: name, type: DefaultTask, dependsOn: setup)
-        writeConfig.doFirst {
-            for (Map.Entry<String, Object> setting : node.config.settings) {
-                if (setting.value == null) {
-                    esConfig.remove(setting.key)
-                } else {
-                    esConfig.put(setting.key, setting.value)
-                }
-            }
-
-            esConfig = configFilter.call(esConfig)
-            File configFile = new File(node.pathConf, 'elasticsearch.yml')
-            logger.info("Configuring ${configFile}")
-            configFile.setText(esConfig.collect { key, value -> "${key}: ${value}" }.join('\n'), 'UTF-8')
-        }
-    }
-
-    /** Adds a task to create keystore */
-    static Task configureCreateKeystoreTask(String name, Project project, Task setup, NodeInfo node) {
-        if (node.config.keystoreSettings.isEmpty() && node.config.keystoreFiles.isEmpty()) {
-            return setup
-        } else {
-            /*
-             * We have to delay building the string as the path will not exist during configuration which will fail on Windows due to
-             * getting the short name requiring the path to already exist.
-             */
-            final Object esKeystoreUtil = "${-> node.binPath().resolve('elasticsearch-keystore').toString()}"
-            return configureExecTask(name, project, setup, node, esKeystoreUtil, 'create')
-        }
-    }
-
-    /** Adds tasks to add settings to the keystore */
-    static Task configureAddKeystoreSettingTasks(String parent, Project project, Task setup, NodeInfo node) {
-        Map kvs = node.config.keystoreSettings
-        Task parentTask = setup
-        /*
-         * We have to delay building the string as the path will not exist during configuration which will fail on Windows due to getting
-         * the short name requiring the path to already exist.
-         */
-        final Object esKeystoreUtil = "${-> node.binPath().resolve('elasticsearch-keystore').toString()}"
-        for (Map.Entry<String, String> entry in kvs) {
-            String key = entry.getKey()
-            String name = taskName(parent, node, 'addToKeystore#' + key)
-            Task t = configureExecTask(name, project, parentTask, node, esKeystoreUtil, 'add', key, '-x')
-            String settingsValue = entry.getValue() // eval this early otherwise it will not use the right value
-            t.doFirst {
-                standardInput = new ByteArrayInputStream(settingsValue.getBytes(StandardCharsets.UTF_8))
-            }
-            parentTask = t
-        }
-        return parentTask
-    }
-
-    /** Adds tasks to add files to the keystore */
-    static Task configureAddKeystoreFileTasks(String parent, Project project, Task setup, NodeInfo node) {
-        Map<String, Object> kvs = node.config.keystoreFiles
-        if (kvs.isEmpty()) {
-            return setup
-        }
-        Task parentTask = setup
-        /*
-         * We have to delay building the string as the path will not exist during configuration which will fail on Windows due to getting
-         * the short name requiring the path to already exist.
-         */
-        final Object esKeystoreUtil = "${-> node.binPath().resolve('elasticsearch-keystore').toString()}"
-        for (Map.Entry<String, Object> entry in kvs) {
-            String key = entry.getKey()
-            String name = taskName(parent, node, 'addToKeystore#' + key)
-            String srcFileName = entry.getValue()
-            Task t = configureExecTask(name, project, parentTask, node, esKeystoreUtil, 'add-file', key, srcFileName)
-            t.doFirst {
-                File srcFile = project.file(srcFileName)
-                if (srcFile.isDirectory()) {
-                    throw new GradleException("Source for keystoreFile must be a file: ${srcFile}")
-                }
-                if (srcFile.exists() == false) {
-                    throw new GradleException("Source file for keystoreFile does not exist: ${srcFile}")
-                }
-            }
-            parentTask = t
-        }
-        return parentTask
-    }
-
-    static Task configureExtraConfigFilesTask(String name, Project project, Task setup, NodeInfo node) {
-        if (node.config.extraConfigFiles.isEmpty()) {
-            return setup
-        }
-        Copy copyConfig = project.tasks.create(name: name, type: Copy, dependsOn: setup)
-        File configDir = new File(node.homeDir, 'config')
-        copyConfig.into(configDir) // copy must always have a general dest dir, even though we don't use it
-        for (Map.Entry<String,Object> extraConfigFile : node.config.extraConfigFiles.entrySet()) {
-            Object extraConfigFileValue = extraConfigFile.getValue()
-            copyConfig.doFirst {
-                // make sure the copy won't be a no-op or act on a directory
-                File srcConfigFile = project.file(extraConfigFileValue)
-                if (srcConfigFile.isDirectory()) {
-                    throw new GradleException("Source for extraConfigFile must be a file: ${srcConfigFile}")
-                }
-                if (srcConfigFile.exists() == false) {
-                    throw new GradleException("Source file for extraConfigFile does not exist: ${srcConfigFile}")
-                }
-            }
-            File destConfigFile = new File(node.homeDir, 'config/' + extraConfigFile.getKey())
-            // wrap source file in closure to delay resolution to execution time
-            copyConfig.from({ extraConfigFileValue }) {
-                // this must be in a closure so it is only applied to the single file specified in from above
-                into(configDir.toPath().relativize(destConfigFile.canonicalFile.parentFile.toPath()).toFile())
-                rename { destConfigFile.name }
-            }
-        }
-        return copyConfig
-    }
-
-    /**
-     * Adds a task to copy plugins to a temp dir, which they will later be installed from.
-     *
-     * For each plugin, if the plugin has rest spec apis in its tests, those api files are also copied
-     * to the test resources for this project.
-     */
-    static Task configureCopyPluginsTask(String name, Project project, Task setup, NodeInfo node, String prefix) {
-        Copy copyPlugins = project.tasks.create(name: name, type: Copy, dependsOn: setup)
-
-        List<FileCollection> pluginFiles = []
-        for (Map.Entry<String, Object> plugin : node.config.plugins.entrySet()) {
-
-            String configurationName = pluginConfigurationName(prefix, plugin.key)
-            Configuration configuration = project.configurations.findByName(configurationName)
-            if (configuration == null) {
-                configuration = project.configurations.create(configurationName)
-            }
-
-            if (plugin.getValue() instanceof Project) {
-                Project pluginProject = plugin.getValue()
-                verifyProjectHasBuildPlugin(name, node.nodeVersion, project, pluginProject)
-
-                project.dependencies.add(configurationName, project.dependencies.project(path: pluginProject.path, configuration: 'zip'))
-                setup.dependsOn(pluginProject.tasks.bundlePlugin)
-
-                // also allow rest tests to use the rest spec from the plugin
-                String copyRestSpecTaskName = pluginTaskName('copy', plugin.getKey(), 'PluginRestSpec')
-                Copy copyRestSpec = project.tasks.findByName(copyRestSpecTaskName)
-                for (File resourceDir : pluginProject.sourceSets.test.resources.srcDirs) {
-                    File restApiDir = new File(resourceDir, 'rest-api-spec/api')
-                    if (restApiDir.exists() == false) continue
-                    if (copyRestSpec == null) {
-                        copyRestSpec = project.tasks.create(name: copyRestSpecTaskName, type: Copy)
-                        copyPlugins.dependsOn(copyRestSpec)
-                        copyRestSpec.into(project.sourceSets.test.output.resourcesDir)
-                    }
-                    copyRestSpec.from(resourceDir).include('rest-api-spec/api/**')
-                }
-            } else {
-                project.dependencies.add(configurationName, "${plugin.getValue()}@zip")
-            }
-
-
-
-            pluginFiles.add(configuration)
-        }
-
-        copyPlugins.into(node.pluginsTmpDir)
-        copyPlugins.from(pluginFiles)
-        return copyPlugins
-    }
-
-    private static String pluginConfigurationName(final String prefix, final String name) {
-        return "_plugin_${prefix}_${name}".replace(':', '_')
-    }
-
-    private static String pluginBwcConfigurationName(final String prefix, final String name) {
-        return "_plugin_bwc_${prefix}_${name}".replace(':', '_')
-    }
-
-    /** Configures task to copy a plugin based on a zip file resolved using dependencies for an older version */
-    static Task configureCopyBwcPluginsTask(String name, Project project, Task setup, NodeInfo node, String prefix) {
-        Configuration bwcPlugins = project.configurations.getByName("${prefix}_elasticsearchBwcPlugins")
-        for (Map.Entry<String, Object> plugin : node.config.plugins.entrySet()) {
-            String configurationName = pluginBwcConfigurationName(prefix, plugin.key)
-            Configuration configuration = project.configurations.findByName(configurationName)
-            if (configuration == null) {
-                configuration = project.configurations.create(configurationName)
-            }
-
-            if (plugin.getValue() instanceof Project) {
-                Project pluginProject = plugin.getValue()
-                verifyProjectHasBuildPlugin(name, node.nodeVersion, project, pluginProject)
-
-                final String depName = findPluginName(pluginProject)
-
-                Dependency dep = bwcPlugins.dependencies.find {
-                    it.name == depName
-                }
-                configuration.dependencies.add(dep)
-            } else {
-                project.dependencies.add(configurationName, "${plugin.getValue()}@zip")
-            }
-        }
-
-        Copy copyPlugins = project.tasks.create(name: name, type: Copy, dependsOn: setup) {
-            from bwcPlugins
-            into node.pluginsTmpDir
-        }
-        return copyPlugins
-    }
-
-    static Task configureInstallModuleTask(String name, Project project, Task setup, NodeInfo node, Project module) {
-        if (node.config.distribution != 'integ-test-zip') {
-            project.logger.info("Not installing modules for $name, ${node.config.distribution} already has them")
-            return setup
-        }
-        if (module.plugins.hasPlugin(PluginBuildPlugin) == false) {
-            throw new GradleException("Task ${name} cannot include module ${module.path} which is not an esplugin")
-        }
-        Copy installModule = project.tasks.create(name, Copy.class)
-        installModule.dependsOn(setup)
-        installModule.dependsOn(module.tasks.bundlePlugin)
-        installModule.into(new File(node.homeDir, "modules/${module.name}"))
-        installModule.from({ project.zipTree(module.tasks.bundlePlugin.outputs.files.singleFile) })
-        return installModule
-    }
-
-    static Task configureInstallPluginTask(String name, Project project, Task setup, NodeInfo node, String pluginName, String prefix) {
-        FileCollection pluginZip;
-        if (node.nodeVersion != Version.fromString(VersionProperties.elasticsearch)) {
-            pluginZip = project.configurations.getByName(pluginBwcConfigurationName(prefix, pluginName))
-        } else {
-            pluginZip = project.configurations.getByName(pluginConfigurationName(prefix, pluginName))
-        }
-        // delay reading the file location until execution time by wrapping in a closure within a GString
-        final Object file = "${-> new File(node.pluginsTmpDir, pluginZip.singleFile.getName()).toURI().toURL().toString()}"
-        /*
-         * We have to delay building the string as the path will not exist during configuration which will fail on Windows due to getting
-         * the short name requiring the path to already exist.
-         */
-        final Object esPluginUtil = "${-> node.binPath().resolve('elasticsearch-plugin').toString()}"
-        final Object[] args = [esPluginUtil, 'install', '--batch', file]
-        return configureExecTask(name, project, setup, node, args)
-    }
-
-    /** Wrapper for command line argument: surrounds comma with double quotes **/
-    private static class EscapeCommaWrapper {
-
-        Object arg
-
-        public String toString() {
-            String s = arg.toString()
-
-            /// Surround strings that contains a comma with double quotes
-            if (s.indexOf(',') != -1) {
-                return "\"${s}\""
-            }
-            return s
-        }
-    }
-
-    /** Adds a task to execute a command to help setup the cluster */
-    static Task configureExecTask(String name, Project project, Task setup, NodeInfo node, Object[] execArgs) {
-        return project.tasks.create(name: name, type: LoggedExec, dependsOn: setup) { Exec exec ->
-            exec.workingDir node.cwd
-            if (useRuntimeJava(project, node)) {
-                exec.environment.put('JAVA_HOME', project.runtimeJavaHome)
-            } else {
-                // force JAVA_HOME to *not* be set
-                exec.environment.remove('JAVA_HOME')
-            }
-            if (Os.isFamily(Os.FAMILY_WINDOWS)) {
-                exec.executable 'cmd'
-                exec.args '/C', 'call'
-                // On Windows the comma character is considered a parameter separator:
-                // argument are wrapped in an ExecArgWrapper that escapes commas
-                exec.args execArgs.collect { a -> new EscapeCommaWrapper(arg: a) }
-            } else {
-                exec.commandLine execArgs
-            }
-        }
-    }
-
-    public static boolean useRuntimeJava(Project project, NodeInfo node) {
-        return (project.isRuntimeJavaHomeSet ||
-                (node.isBwcNode == false && node.nodeVersion.before(Version.fromString("7.0.0"))) ||
-                node.config.distribution == 'integ-test-zip')
-    }
-
-    /** Adds a task to start an elasticsearch node with the given configuration */
-    static Task configureStartTask(String name, Project project, Task setup, NodeInfo node) {
-        // this closure is converted into ant nodes by groovy's AntBuilder
-        Closure antRunner = { AntBuilder ant ->
-            ant.exec(executable: node.executable, spawn: node.config.daemonize, newenvironment: true,
-                     dir: node.cwd, taskname: 'elasticsearch') {
-                node.env.each { key, value -> env(key: key, value: value) }
-                if (useRuntimeJava(project, node)) {
-                    env(key: 'JAVA_HOME', value: project.runtimeJavaHome)
-                }
-                node.args.each { arg(value: it) }
-                if (Os.isFamily(Os.FAMILY_WINDOWS)) {
-                    // Having no TMP on Windows defaults to C:\Windows and permission errors
-                    // Since we configure ant to run with a new  environment above, we need to explicitly pass this
-                    String tmp = System.getenv("TMP")
-                    assert tmp != null
-                    env(key: "TMP", value: tmp)
-                }
-            }
-        }
-
-        // this closure is the actual code to run elasticsearch
-        Closure elasticsearchRunner = {
-            // Due to how ant exec works with the spawn option, we lose all stdout/stderr from the
-            // process executed. To work around this, when spawning, we wrap the elasticsearch start
-            // command inside another shell script, which simply internally redirects the output
-            // of the real elasticsearch script. This allows ant to keep the streams open with the
-            // dummy process, but us to have the output available if there is an error in the
-            // elasticsearch start script
-            if (node.config.daemonize) {
-                node.writeWrapperScript()
-            }
-
-            node.getCommandString().eachLine { line -> logger.info(line) }
-
-            if (logger.isInfoEnabled() || node.config.daemonize == false) {
-                runAntCommand(project, antRunner, System.out, System.err)
-            } else {
-                // buffer the output, we may not need to print it
-                PrintStream captureStream = new PrintStream(node.buffer, true, "UTF-8")
-                runAntCommand(project, antRunner, captureStream, captureStream)
-            }
-        }
-
-        Task start = project.tasks.create(name: name, type: DefaultTask, dependsOn: setup)
-        if (node.javaVersion != null) {
-            BuildPlugin.requireJavaHome(start, node.javaVersion)
-        }
-        start.doLast(elasticsearchRunner)
-        start.doFirst {
-            // If the node runs in a FIPS 140-2 JVM, the BCFKS default keystore will be password protected
-            if (project.inFipsJvm){
-                node.config.systemProperties.put('javax.net.ssl.trustStorePassword', 'password')
-                node.config.systemProperties.put('javax.net.ssl.keyStorePassword', 'password')
-            }
-
-            // Configure ES JAVA OPTS - adds system properties, assertion flags, remote debug etc
-            List<String> esJavaOpts = [node.env.get('ES_JAVA_OPTS', '')]
-            String collectedSystemProperties = node.config.systemProperties.collect { key, value -> "-D${key}=${value}" }.join(" ")
-            esJavaOpts.add(collectedSystemProperties)
-            esJavaOpts.add(node.config.jvmArgs)
-            if (Boolean.parseBoolean(System.getProperty('tests.asserts', 'true'))) {
-                // put the enable assertions options before other options to allow
-                // flexibility to disable assertions for specific packages or classes
-                // in the cluster-specific options
-                esJavaOpts.add("-ea")
-                esJavaOpts.add("-esa")
-            }
-            // we must add debug options inside the closure so the config is read at execution time, as
-            // gradle task options are not processed until the end of the configuration phase
-            if (node.config.debug) {
-                println 'Running elasticsearch in debug mode, suspending until connected on port 8000'
-                esJavaOpts.add('-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=8000')
-            }
-            node.env['ES_JAVA_OPTS'] = esJavaOpts.join(" ")
-
-            //
-            project.logger.info("Starting node in ${node.clusterName} distribution: ${node.config.distribution}")
-        }
-        return start
-    }
-
-    static Task configureWaitTask(String name, Project project, List<NodeInfo> nodes, List<Task> startTasks, int waitSeconds) {
-        Task wait = project.tasks.create(name: name, dependsOn: startTasks)
-        wait.doLast {
-
-            Collection<String> unicastHosts = new HashSet<>()
-            nodes.forEach { node ->
-                unicastHosts.addAll(node.config.otherUnicastHostAddresses.call())
-                String unicastHost = node.config.unicastTransportUri(node, null, project.createAntBuilder())
-                if (unicastHost != null) {
-                    unicastHosts.add(unicastHost)
-                }
-            }
-            String unicastHostsTxt = String.join("\n", unicastHosts)
-            nodes.forEach { node ->
-                node.pathConf.toPath().resolve("unicast_hosts.txt").setText(unicastHostsTxt)
-            }
-
-            ant.waitfor(maxwait: "${waitSeconds}", maxwaitunit: 'second', checkevery: '500', checkeveryunit: 'millisecond', timeoutproperty: "failed${name}") {
-                or {
-                    for (NodeInfo node : nodes) {
-                        resourceexists {
-                            file(file: node.failedMarker.toString())
-                        }
-                    }
-                    and {
-                        for (NodeInfo node : nodes) {
-                            resourceexists {
-                                file(file: node.pidFile.toString())
-                            }
-                            resourceexists {
-                                file(file: node.httpPortsFile.toString())
-                            }
-                            resourceexists {
-                                file(file: node.transportPortsFile.toString())
-                            }
-                        }
-                    }
-                }
-            }
-            if (ant.properties.containsKey("failed${name}".toString())) {
-                waitFailed(project, nodes, logger, "Failed to start elasticsearch: timed out after ${waitSeconds} seconds")
-            }
-
-            boolean anyNodeFailed = false
-            for (NodeInfo node : nodes) {
-                if (node.failedMarker.exists()) {
-                    logger.error("Failed to start elasticsearch: ${node.failedMarker.toString()} exists")
-                    anyNodeFailed = true
-                }
-            }
-            if (anyNodeFailed) {
-                waitFailed(project, nodes, logger, 'Failed to start elasticsearch')
-            }
-
-            // make sure all files exist otherwise we haven't fully started up
-            boolean missingFile = false
-            for (NodeInfo node : nodes) {
-                missingFile |= node.pidFile.exists() == false
-                missingFile |= node.httpPortsFile.exists() == false
-                missingFile |= node.transportPortsFile.exists() == false
-            }
-            if (missingFile) {
-                waitFailed(project, nodes, logger, 'Elasticsearch did not complete startup in time allotted')
-            }
-
-            // go through each node checking the wait condition
-            for (NodeInfo node : nodes) {
-                // first bind node info to the closure, then pass to the ant runner so we can get good logging
-                Closure antRunner = node.config.waitCondition.curry(node)
-
-                boolean success
-                if (logger.isInfoEnabled()) {
-                    success = runAntCommand(project, antRunner, System.out, System.err)
-                } else {
-                    PrintStream captureStream = new PrintStream(node.buffer, true, "UTF-8")
-                    success = runAntCommand(project, antRunner, captureStream, captureStream)
-                }
-
-                if (success == false) {
-                    waitFailed(project, nodes, logger, 'Elasticsearch cluster failed to pass wait condition')
-                }
-            }
-        }
-        return wait
-    }
-
-    static void waitFailed(Project project, List<NodeInfo> nodes, Logger logger, String msg) {
-        for (NodeInfo node : nodes) {
-            if (logger.isInfoEnabled() == false) {
-                // We already log the command at info level. No need to do it twice.
-                node.getCommandString().eachLine { line -> logger.error(line) }
-            }
-            logger.error("Node ${node.nodeNum} output:")
-            logger.error("|-----------------------------------------")
-            logger.error("|  failure marker exists: ${node.failedMarker.exists()}")
-            logger.error("|  pid file exists: ${node.pidFile.exists()}")
-            logger.error("|  http ports file exists: ${node.httpPortsFile.exists()}")
-            logger.error("|  transport ports file exists: ${node.transportPortsFile.exists()}")
-            // the waitfor failed, so dump any output we got (if info logging this goes directly to stdout)
-            logger.error("|\n|  [ant output]")
-            node.buffer.toString('UTF-8').eachLine { line -> logger.error("|    ${line}") }
-            // also dump the log file for the startup script (which will include ES logging output to stdout)
-            if (node.startLog.exists()) {
-                logger.error("|\n|  [log]")
-                node.startLog.eachLine { line -> logger.error("|    ${line}") }
-            }
-            if (node.pidFile.exists() && node.failedMarker.exists() == false &&
-                (node.httpPortsFile.exists() == false || node.transportPortsFile.exists() == false)) {
-                logger.error("|\n|  [jstack]")
-                String pid = node.pidFile.getText('UTF-8')
-                ByteArrayOutputStream output = new ByteArrayOutputStream()
-                project.exec {
-                    commandLine = ["${project.runtimeJavaHome}/bin/jstack", pid]
-                    standardOutput = output
-                }
-                output.toString('UTF-8').eachLine { line -> logger.error("|    ${line}") }
-            }
-            logger.error("|-----------------------------------------")
-        }
-        throw new GradleException(msg)
-    }
-
-    /** Adds a task to check if the process with the given pidfile is actually elasticsearch */
-    static Task configureCheckPreviousTask(String name, Project project, Object depends, NodeInfo node) {
-        return project.tasks.create(name: name, type: Exec, dependsOn: depends) {
-            onlyIf { node.pidFile.exists() }
-            // the pid file won't actually be read until execution time, since the read is wrapped within an inner closure of the GString
-            ext.pid = "${ -> node.pidFile.getText('UTF-8').trim()}"
-            final File jps = Jvm.forHome(project.runtimeJavaHome).getExecutable('jps')
-            commandLine jps, '-l'
-            standardOutput = new ByteArrayOutputStream()
-            doLast {
-                String out = standardOutput.toString()
-                if (out.contains("${ext.pid} org.elasticsearch.bootstrap.Elasticsearch") == false) {
-                    logger.error('jps -l')
-                    logger.error(out)
-                    logger.error("pid file: ${node.pidFile}")
-                    logger.error("pid: ${ext.pid}")
-                    throw new GradleException("jps -l did not report any process with org.elasticsearch.bootstrap.Elasticsearch\n" +
-                            "Did you run gradle clean? Maybe an old pid file is still lying around.")
-                } else {
-                    logger.info(out)
-                }
-            }
-        }
-    }
-
-    /** Adds a task to kill an elasticsearch node with the given pidfile */
-    static Task configureStopTask(String name, Project project, Object depends, NodeInfo node) {
-        return project.tasks.create(name: name, type: LoggedExec, dependsOn: depends) {
-            onlyIf { node.pidFile.exists() }
-            // the pid file won't actually be read until execution time, since the read is wrapped within an inner closure of the GString
-            ext.pid = "${ -> node.pidFile.getText('UTF-8').trim()}"
-            doFirst {
-                logger.info("Shutting down external node with pid ${pid}")
-            }
-            if (Os.isFamily(Os.FAMILY_WINDOWS)) {
-                executable 'Taskkill'
-                args '/PID', pid, '/F'
-            } else {
-                executable 'kill'
-                args '-9', pid
-            }
-            doLast {
-                project.delete(node.pidFile)
-                // Large tests can exhaust disk space, clean up jdk from the distribution to save some space
-                project.delete(new File(node.homeDir, "jdk"))
-            }
-        }
-    }
-
-    /** Returns a unique task name for this task and node configuration */
-    static String taskName(String prefix, NodeInfo node, String action) {
-        if (node.config.numNodes > 1) {
-            return "${prefix}#node${node.nodeNum}.${action}"
-        } else {
-            return "${prefix}#${action}"
-        }
-    }
-
-    public static String pluginTaskName(String action, String name, String suffix) {
-        // replace every dash followed by a character with just the uppercase character
-        String camelName = name.replaceAll(/-(\w)/) { _, c -> c.toUpperCase(Locale.ROOT) }
-        return action + camelName[0].toUpperCase(Locale.ROOT) + camelName.substring(1) + suffix
-    }
-
-    /** Runs an ant command, sending output to the given out and error streams */
-    static Object runAntCommand(Project project, Closure command, PrintStream outputStream, PrintStream errorStream) {
-        DefaultLogger listener = new DefaultLogger(
-                errorPrintStream: errorStream,
-                outputPrintStream: outputStream,
-                messageOutputLevel: org.apache.tools.ant.Project.MSG_INFO)
-
-        AntBuilder ant = project.createAntBuilder()
-        ant.project.addBuildListener(listener)
-        Object retVal = command(ant)
-        ant.project.removeBuildListener(listener)
-        return retVal
-    }
-
-    static void verifyProjectHasBuildPlugin(String name, Version version, Project project, Project pluginProject) {
-        if (pluginProject.plugins.hasPlugin(PluginBuildPlugin) == false) {
-            throw new GradleException("Task [${name}] cannot add plugin [${pluginProject.path}] with version [${version}] to project's " +
-                    "[${project.path}] dependencies: the plugin is not an esplugin")
-        }
-    }
-
-    /** Find the plugin name in the given project. */
-    static String findPluginName(Project pluginProject) {
-        PluginPropertiesExtension extension = pluginProject.extensions.findByName('esplugin')
-        return extension.name
-    }
-
-    /** Find the current OS */
-    static String getOs() {
-        String os = "linux"
-        if (Os.isFamily(Os.FAMILY_WINDOWS)) {
-            os = "windows"
-        } else if (Os.isFamily(Os.FAMILY_MAC)) {
-            os = "darwin"
-        }
-        return os
-    }
-}

+ 0 - 297
buildSrc/src/main/groovy/org/elasticsearch/gradle/test/NodeInfo.groovy

@@ -1,297 +0,0 @@
-/*
- * Licensed to Elasticsearch under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.elasticsearch.gradle.test
-
-import com.sun.jna.Native
-import com.sun.jna.WString
-import org.apache.tools.ant.taskdefs.condition.Os
-import org.elasticsearch.gradle.Version
-import org.elasticsearch.gradle.VersionProperties
-import org.gradle.api.Project
-
-import java.nio.file.Files
-import java.nio.file.Path
-import java.nio.file.Paths
-/**
- * A container for the files and configuration associated with a single node in a test cluster.
- */
-class NodeInfo {
-    /** Gradle project this node is part of */
-    Project project
-
-    /** common configuration for all nodes, including this one */
-    ClusterConfiguration config
-
-    /** node number within the cluster, for creating unique names and paths */
-    int nodeNum
-
-    /** name of the cluster this node is part of */
-    String clusterName
-
-    /** root directory all node files and operations happen under */
-    File baseDir
-
-    /** shared data directory all nodes share */
-    File sharedDir
-
-    /** the pid file the node will use */
-    File pidFile
-
-    /** a file written by elasticsearch containing the ports of each bound address for http */
-    File httpPortsFile
-
-    /** a file written by elasticsearch containing the ports of each bound address for transport */
-    File transportPortsFile
-
-    /** elasticsearch home dir */
-    File homeDir
-
-    /** config directory */
-    File pathConf
-
-    /** data directory (as an Object, to allow lazy evaluation) */
-    Object dataDir
-
-    /** THE config file */
-    File configFile
-
-    /** working directory for the node process */
-    File cwd
-
-    /** file that if it exists, indicates the node failed to start */
-    File failedMarker
-
-    /** stdout/stderr log of the elasticsearch process for this node */
-    File startLog
-
-    /** directory to install plugins from */
-    File pluginsTmpDir
-
-    /** Major version of java this node runs with, or {@code null} if using the runtime java version */
-    Integer javaVersion
-
-    /** environment variables to start the node with */
-    Map<String, String> env
-
-    /** arguments to start the node with */
-    List<String> args
-
-    /** Executable to run the bin/elasticsearch with, either cmd or sh */
-    String executable
-
-    /** Path to the elasticsearch start script */
-    private Object esScript
-
-    /** script to run when running in the background */
-    private File wrapperScript
-
-    /** buffer for ant output when starting this node */
-    ByteArrayOutputStream buffer = new ByteArrayOutputStream()
-
-    /** the version of elasticsearch that this node runs */
-    Version nodeVersion
-
-    /** true if the node is not the current version */
-    boolean isBwcNode
-
-    /** Holds node configuration for part of a test cluster. */
-    NodeInfo(ClusterConfiguration config, int nodeNum, Project project, String prefix, String nodeVersion, File sharedDir) {
-        this.config = config
-        this.nodeNum = nodeNum
-        this.project = project
-        this.sharedDir = sharedDir
-        if (config.clusterName != null) {
-            clusterName = config.clusterName
-        } else {
-            clusterName = project.path.replace(':', '_').substring(1) + '_' + prefix
-        }
-        baseDir = new File(project.buildDir, "cluster/${prefix} node${nodeNum}")
-        pidFile = new File(baseDir, 'es.pid')
-        this.nodeVersion = Version.fromString(nodeVersion)
-        this.isBwcNode = this.nodeVersion.before(VersionProperties.elasticsearch)
-        homeDir = new File(baseDir, "elasticsearch-${nodeVersion}")
-        pathConf = new File(homeDir, 'config')
-        if (config.dataDir != null) {
-            dataDir = "${config.dataDir(nodeNum)}"
-        } else {
-            dataDir = new File(homeDir, "data")
-        }
-        configFile = new File(pathConf, 'elasticsearch.yml')
-        // even for rpm/deb, the logs are under home because we dont start with real services
-        File logsDir = new File(homeDir, 'logs')
-        httpPortsFile = new File(logsDir, 'http.ports')
-        transportPortsFile = new File(logsDir, 'transport.ports')
-        cwd = new File(baseDir, "cwd")
-        failedMarker = new File(cwd, 'run.failed')
-        startLog = new File(cwd, 'run.log')
-        pluginsTmpDir = new File(baseDir, "plugins tmp")
-
-        args = []
-        if (Os.isFamily(Os.FAMILY_WINDOWS)) {
-            executable = 'cmd'
-            args.add('/C')
-            args.add('"') // quote the entire command
-            wrapperScript = new File(cwd, "run.bat")
-            /*
-             * We have to delay building the string as the path will not exist during configuration which will fail on Windows due to
-             * getting the short name requiring the path to already exist.
-             */
-            esScript = "${-> binPath().resolve('elasticsearch.bat').toString()}"
-        } else {
-            executable = 'bash'
-            wrapperScript = new File(cwd, "run")
-            esScript = binPath().resolve('elasticsearch')
-        }
-        if (config.daemonize) {
-            if (Os.isFamily(Os.FAMILY_WINDOWS)) {
-                /*
-                 * We have to delay building the string as the path will not exist during configuration which will fail on Windows due to
-                 * getting the short name requiring the path to already exist.
-                 */
-                args.add("${-> getShortPathName(wrapperScript.toString())}")
-            } else {
-                args.add("${wrapperScript}")
-            }
-        } else {
-            args.add("${esScript}")
-        }
-
-
-        if (this.nodeVersion.before("6.2.0")) {
-            javaVersion = 8
-        } else if (this.nodeVersion.onOrAfter("6.2.0") && this.nodeVersion.before("6.3.0")) {
-            javaVersion = 9
-        } else if (this.nodeVersion.onOrAfter("6.3.0") && this.nodeVersion.before("6.5.0")) {
-            javaVersion = 10
-        }
-
-        args.addAll("-E", "node.portsfile=true")
-        env = [:]
-        env.putAll(config.environmentVariables)
-        for (Map.Entry<String, String> property : System.properties.entrySet()) {
-            if (property.key.startsWith('tests.es.')) {
-                args.add("-E")
-                args.add("${property.key.substring('tests.es.'.size())}=${property.value}")
-            }
-        }
-        if (Os.isFamily(Os.FAMILY_WINDOWS)) {
-            /*
-             * We have to delay building the string as the path will not exist during configuration which will fail on Windows due to
-             * getting the short name requiring the path to already exist.
-             */
-            env.put('ES_PATH_CONF', "${-> getShortPathName(pathConf.toString())}")
-        }
-        else {
-            env.put('ES_PATH_CONF', pathConf)
-        }
-        if (!System.properties.containsKey("tests.es.path.data")) {
-            if (Os.isFamily(Os.FAMILY_WINDOWS)) {
-                /*
-                 * We have to delay building the string as the path will not exist during configuration which will fail on Windows due to
-                 * getting the short name requiring the path to already exist. This one is extra tricky because usually we rely on the node
-                 * creating its data directory on startup but we simply can not do that here because getting the short path name requires
-                 * the directory to already exist. Therefore, we create this directory immediately before getting the short name.
-                 */
-                args.addAll("-E", "path.data=${-> Files.createDirectories(Paths.get(dataDir.toString())); getShortPathName(dataDir.toString())}")
-            } else {
-                args.addAll("-E", "path.data=${-> dataDir.toString()}")
-            }
-        }
-        if (Os.isFamily(Os.FAMILY_WINDOWS)) {
-            args.add('"') // end the entire command, quoted
-        }
-    }
-
-    Path binPath() {
-        if (Os.isFamily(Os.FAMILY_WINDOWS)) {
-            return Paths.get(getShortPathName(new File(homeDir, 'bin').toString()))
-        } else {
-            return Paths.get(new File(homeDir, 'bin').toURI())
-        }
-    }
-
-    static String getShortPathName(String path) {
-        assert Os.isFamily(Os.FAMILY_WINDOWS)
-        final WString longPath = new WString("\\\\?\\" + path)
-        // first we get the length of the buffer needed
-        final int length = JNAKernel32Library.getInstance().GetShortPathNameW(longPath, null, 0)
-        if (length == 0) {
-            throw new IllegalStateException("path [" + path + "] encountered error [" + Native.getLastError() + "]")
-        }
-        final char[] shortPath = new char[length]
-        // knowing the length of the buffer, now we get the short name
-        if (JNAKernel32Library.getInstance().GetShortPathNameW(longPath, shortPath, length) == 0) {
-            throw new IllegalStateException("path [" + path + "] encountered error [" + Native.getLastError() + "]")
-        }
-        // we have to strip the \\?\ away from the path for cmd.exe
-        return Native.toString(shortPath).substring(4)
-    }
-
-    /** Returns debug string for the command that started this node. */
-    String getCommandString() {
-        String esCommandString = "\nNode ${nodeNum} configuration:\n"
-        esCommandString += "|-----------------------------------------\n"
-        esCommandString += "|  cwd: ${cwd}\n"
-        esCommandString += "|  command: ${executable} ${args.join(' ')}\n"
-        esCommandString += '|  environment:\n'
-        env.each { k, v -> esCommandString += "|    ${k}: ${v}\n" }
-        if (config.daemonize) {
-            esCommandString += "|\n|  [${wrapperScript.name}]\n"
-            wrapperScript.eachLine('UTF-8', { line -> esCommandString += "    ${line}\n"})
-        }
-        esCommandString += '|\n|  [elasticsearch.yml]\n'
-        configFile.eachLine('UTF-8', { line -> esCommandString += "|    ${line}\n" })
-        esCommandString += "|-----------------------------------------"
-        return esCommandString
-    }
-
-    void writeWrapperScript() {
-        String argsPasser = '"$@"'
-        String exitMarker = "; if [ \$? != 0 ]; then touch run.failed; fi"
-        if (Os.isFamily(Os.FAMILY_WINDOWS)) {
-            argsPasser = '%*'
-            exitMarker = "\r\n if \"%errorlevel%\" neq \"0\" ( type nul >> run.failed )"
-        }
-        wrapperScript.setText("\"${esScript}\" ${argsPasser} > run.log 2>&1 ${exitMarker}", 'UTF-8')
-    }
-
-    /** Returns an address and port suitable for a uri to connect to this node over http */
-    String httpUri() {
-        return httpPortsFile.readLines("UTF-8").get(0)
-    }
-
-    /** Returns an address and port suitable for a uri to connect to this node over transport protocol */
-    String transportUri() {
-        return transportPortsFile.readLines("UTF-8").get(0)
-    }
-
-    /** Returns the file which contains the transport protocol ports for this node */
-    File getTransportPortsFile() {
-        return transportPortsFile
-    }
-
-    /** Returns the data directory for this node */
-    File getDataDir() {
-        if (!(dataDir instanceof File)) {
-            return new File(dataDir)
-        }
-        return dataDir
-    }
-}

+ 0 - 41
buildSrc/src/main/groovy/org/elasticsearch/gradle/test/RunTask.groovy

@@ -1,41 +0,0 @@
-package org.elasticsearch.gradle.test
-
-import org.gradle.api.DefaultTask
-import org.gradle.api.Task
-import org.gradle.api.tasks.Internal
-import org.gradle.api.tasks.options.Option
-import org.gradle.util.ConfigureUtil
-
-class RunTask extends DefaultTask {
-
-    @Internal
-    ClusterConfiguration clusterConfig
-
-    RunTask() {
-        description = "Runs elasticsearch with '${project.path}'"
-        group = 'Verification'
-        clusterConfig = new ClusterConfiguration(project)
-        clusterConfig.httpPort = 9200
-        clusterConfig.transportPort = 9300
-        clusterConfig.daemonize = false
-        clusterConfig.distribution = 'default'
-        project.afterEvaluate {
-            ClusterFormationTasks.setup(project, name, this, clusterConfig)
-        }
-    }
-
-    @Option(
-        option = "debug-jvm",
-        description = "Enable debugging configuration, to allow attaching a debugger to elasticsearch."
-    )
-    void setDebug(boolean enabled) {
-        clusterConfig.debug = enabled;
-    }
-
-    /** Configure the cluster that will be run. */
-    @Override
-    Task configure(Closure closure) {
-        ConfigureUtil.configure(closure, clusterConfig)
-        return this
-    }
-}

+ 1 - 1
buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/ElasticsearchCluster.java

@@ -104,7 +104,7 @@ public class ElasticsearchCluster implements TestClusterConfiguration, Named {
         }
     }
 
-    private ElasticsearchNode getFirstNode() {
+    ElasticsearchNode getFirstNode() {
         return nodes.getAt(clusterName + "-0");
     }
 

+ 29 - 6
buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/ElasticsearchNode.java

@@ -148,6 +148,8 @@ public class ElasticsearchNode implements TestClusterConfiguration {
     private volatile Process esProcess;
     private Function<String, String> nameCustomization = Function.identity();
     private boolean isWorkingDirConfigured = false;
+    private String httpPort = "0";
+    private String transportPort = "0";
 
     ElasticsearchNode(String path, String name, Project project, ReaperService reaper, File workingDirBase) {
         this.path = path;
@@ -359,8 +361,7 @@ public class ElasticsearchNode implements TestClusterConfiguration {
 
     @Override
     public void freeze() {
-        requireNonNull(distributions, "null distribution passed when configuring test cluster `" + this + "`");
-        requireNonNull(javaHome, "null javaHome passed when configuring test cluster `" + this + "`");
+        requireNonNull(testDistribution, "null testDistribution passed when configuring test cluster `" + this + "`");
         LOGGER.info("Locking configuration of `{}`", this);
         configurationFrozen.set(true);
     }
@@ -637,7 +638,9 @@ public class ElasticsearchNode implements TestClusterConfiguration {
 
     private Map<String, String> getESEnvironment() {
         Map<String, String> defaultEnv = new HashMap<>();
-        defaultEnv.put("JAVA_HOME", getJavaHome().getAbsolutePath());
+        if ( getJavaHome() != null) {
+            defaultEnv.put("JAVA_HOME", getJavaHome().getAbsolutePath());
+        }
         defaultEnv.put("ES_PATH_CONF", configFile.getParent().toString());
         String systemPropertiesString = "";
         if (systemProperties.isEmpty() == false) {
@@ -696,9 +699,11 @@ public class ElasticsearchNode implements TestClusterConfiguration {
         // Don't inherit anything from the environment for as that would  lack reproducibility
         environment.clear();
         environment.putAll(getESEnvironment());
+
         // don't buffer all in memory, make sure we don't block on the default pipes
         processBuilder.redirectError(ProcessBuilder.Redirect.appendTo(esStderrFile.toFile()));
         processBuilder.redirectOutput(ProcessBuilder.Redirect.appendTo(esStdoutFile.toFile()));
+
         LOGGER.info("Running `{}` in `{}` for {} env: {}", command, workingDir, this, environment);
         try {
             esProcess = processBuilder.start();
@@ -988,11 +993,11 @@ public class ElasticsearchNode implements TestClusterConfiguration {
         defaultConfig.put("path.shared_data", workingDir.resolve("sharedData").toString());
         defaultConfig.put("node.attr.testattr", "test");
         defaultConfig.put("node.portsfile", "true");
-        defaultConfig.put("http.port", "0");
+        defaultConfig.put("http.port", httpPort);
         if (getVersion().onOrAfter(Version.fromString("6.7.0"))) {
-            defaultConfig.put("transport.port", "0");
+            defaultConfig.put("transport.port", transportPort);
         } else {
-            defaultConfig.put("transport.tcp.port", "0");
+            defaultConfig.put("transport.tcp.port", transportPort);
         }
         // Default the watermarks to absurdly low to prevent the tests from failing on nodes without enough disk space
         defaultConfig.put("cluster.routing.allocation.disk.watermark.low", "1b");
@@ -1287,6 +1292,24 @@ public class ElasticsearchNode implements TestClusterConfiguration {
         }
     }
 
+    void setHttpPort(String httpPort) {
+        this.httpPort = httpPort;
+    }
+
+    void setTransportPort(String transportPort) {
+        this.transportPort = transportPort;
+    }
+
+    @Internal
+    Path getEsStdoutFile() {
+        return esStdoutFile;
+    }
+
+    @Internal
+    Path getEsStderrFile() {
+        return esStderrFile;
+    }
+
     private static class FileEntry implements Named {
         private String name;
         private File file;

+ 73 - 0
buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/RunTask.java

@@ -0,0 +1,73 @@
+package org.elasticsearch.gradle.testclusters;
+
+import org.gradle.api.logging.Logger;
+import org.gradle.api.logging.Logging;
+import org.gradle.api.tasks.Input;
+import org.gradle.api.tasks.TaskAction;
+import org.gradle.api.tasks.options.Option;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.HashSet;
+import java.util.Set;
+
+public class RunTask extends DefaultTestClustersTask {
+
+    private static final Logger logger = Logging.getLogger(RunTask.class);
+
+    private Boolean debug = false;
+
+    @Option(
+        option = "debug-jvm",
+        description = "Enable debugging configuration, to allow attaching a debugger to elasticsearch."
+    )
+    public void setDebug(boolean enabled) {
+        this.debug = debug;
+    }
+
+    @Input
+    public Boolean getDebug() {
+        return debug;
+    }
+
+    @Override
+    public void beforeStart() {
+        int debugPort = 8000;
+        int httpPort = 9200;
+        int transportPort = 9300;
+        for (ElasticsearchCluster cluster : getClusters()) {
+            cluster.getFirstNode().setHttpPort(String.valueOf(httpPort));
+            httpPort++;
+            cluster.getFirstNode().setTransportPort(String.valueOf(transportPort));
+            transportPort++;
+            for (ElasticsearchNode node : cluster.getNodes()) {
+                if (debug) {
+                    logger.lifecycle(
+                        "Running elasticsearch in debug mode, {} suspending until connected on debugPort {}",
+                        node, debugPort
+                    );
+                    node.jvmArgs("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=" + debugPort);
+                    debugPort += 1;
+                }
+            }
+        }
+    }
+
+    @TaskAction
+    public void runAndWait() throws IOException {
+        Set<BufferedReader> toRead = new HashSet<>();
+        for (ElasticsearchCluster cluster : getClusters()) {
+            for (ElasticsearchNode node : cluster.getNodes()) {
+                toRead.add(Files.newBufferedReader(node.getEsStdoutFile()));
+            }
+        }
+        while (Thread.currentThread().isInterrupted() == false) {
+            for (BufferedReader bufferedReader : toRead) {
+                if (bufferedReader.ready()) {
+                    logger.lifecycle(bufferedReader.readLine());
+                }
+            }
+        }
+    }
+}

+ 5 - 1
buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/TestClustersAware.java

@@ -18,9 +18,13 @@ interface TestClustersAware extends Task {
             );
         }
 
-        cluster.getNodes().stream().flatMap(node -> node.getDistributions().stream()).forEach( distro ->
+        cluster.getNodes().stream().flatMap(node -> node.getDistributions().stream()).forEach(distro ->
             dependsOn(distro.getExtracted())
         );
         getClusters().add(cluster);
     }
+
+    default void beforeStart() {
+    }
+
 }

+ 3 - 1
buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/TestClustersPlugin.java

@@ -123,7 +123,9 @@ public class TestClustersPlugin implements Plugin<Project> {
                         return;
                     }
                     // we only start the cluster before the actions, so we'll not start it if the task is up-to-date
-                    ((TestClustersAware) task).getClusters().forEach(registry::maybeStartCluster);
+                    TestClustersAware awareTask = (TestClustersAware) task;
+                    awareTask.beforeStart();
+                    awareTask.getClusters().forEach(registry::maybeStartCluster);
                 }
                 @Override
                 public void afterActions(Task task) {}

+ 25 - 19
distribution/build.gradle

@@ -23,11 +23,13 @@ import org.elasticsearch.gradle.ConcatFilesTask
 import org.elasticsearch.gradle.MavenFilteringHack
 import org.elasticsearch.gradle.NoticeTask
 import org.elasticsearch.gradle.VersionProperties
-import org.elasticsearch.gradle.test.RunTask
+import org.elasticsearch.gradle.testclusters.RunTask
 
 import java.nio.file.Files
 import java.nio.file.Path
 
+apply plugin: 'elasticsearch.testclusters'
+
 /*****************************************************************************
  *                  Third party dependencies report                          *
  *****************************************************************************/
@@ -414,28 +416,32 @@ configure(subprojects.findAll { ['archives', 'packages'].contains(it.name) }) {
 
 }
 
-task run(type: RunTask) {
-  distribution = System.getProperty('run.distribution', 'default')
-  if (distribution == 'default') {
-    String licenseType = System.getProperty("run.license_type", "basic")
-    if (licenseType == 'trial') {
-      setting 'xpack.ml.enabled', 'true'
-      setting 'xpack.graph.enabled', 'true'
-      setting 'xpack.watcher.enabled', 'true'
-      setting 'xpack.license.self_generated.type', 'trial'
-    } else if (licenseType != 'basic') {
-      throw new IllegalArgumentException("Unsupported self-generated license type: [" + licenseType + "[basic] or [trial].")
+testClusters {
+  runTask {
+    testDistribution = System.getProperty('run.distribution', 'default')
+    if (System.getProperty('run.distribution', 'default') == 'default') {
+      String licenseType = System.getProperty("run.license_type", "basic")
+      if (licenseType == 'trial') {
+        setting 'xpack.ml.enabled', 'true'
+        setting 'xpack.graph.enabled', 'true'
+        setting 'xpack.watcher.enabled', 'true'
+        setting 'xpack.license.self_generated.type', 'trial'
+      } else if (licenseType != 'basic') {
+        throw new IllegalArgumentException("Unsupported self-generated license type: [" + licenseType + "[basic] or [trial].")
+      }
+      setting 'xpack.security.enabled', 'true'
+      setting 'xpack.monitoring.enabled', 'true'
+      setting 'xpack.sql.enabled', 'true'
+      setting 'xpack.rollup.enabled', 'true'
+      keystore 'bootstrap.password', 'password'
     }
-    setupCommand 'setupTestAdmin',
-            'bin/elasticsearch-users', 'useradd', 'elastic-admin', '-p', 'elastic-password', '-r', 'superuser'
-    setting 'xpack.security.enabled', 'true'
-    setting 'xpack.monitoring.enabled', 'true'
-    setting 'xpack.sql.enabled', 'true'
-    setting 'xpack.rollup.enabled', 'true'
-    keystoreSetting 'bootstrap.password', 'password'
   }
 }
 
+task run(type: RunTask) {
+  useCluster testClusters.runTask;
+}
+
 /**
  * Build some variables that are replaced in the packages. This includes both
  * scripts like bin/elasticsearch and bin/elasticsearch-plugin that a user might run and also

+ 0 - 5
modules/build.gradle

@@ -26,11 +26,6 @@ configure(subprojects.findAll { it.parent.path == project.path }) {
     // for local ES plugins, the name of the plugin is the same as the directory
     name project.name
   }
-  
-  run {
-    // these cannot be run with the normal distribution, since they are included in it!
-    distribution = 'integ-test-zip'
-  }
 
   if (project.file('src/main/packaging').exists()) {
     throw new InvalidModelException("Modules cannot contain packaging files") 

+ 14 - 12
modules/lang-painless/build.gradle

@@ -16,10 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-
-import org.elasticsearch.gradle.test.ClusterConfiguration
-import org.elasticsearch.gradle.test.ClusterFormationTasks
-
+import org.elasticsearch.gradle.testclusters.DefaultTestClustersTask;
 esplugin {
   description 'An easy, safe and fast scripting language for Elasticsearch'
   classname 'org.elasticsearch.painless.PainlessPlugin'
@@ -76,16 +73,21 @@ dependencies {
   docCompile project(':modules:lang-painless')
 }
 
-ClusterConfiguration clusterConfig = project.extensions.create("generateContextCluster", ClusterConfiguration.class, project)
-gradle.projectsEvaluated {
-  project.ext.generateContextNodes = ClusterFormationTasks.setup(project, "generateContextCluster", generateContextDoc, clusterConfig)
+testClusters {
+  generateContextCluster {
+     testDistribution = 'DEFAULT'
+  }
 }
-clusterConfig.distribution = 'default'
 
-task generateContextDoc(type: JavaExec) {
-  main = 'org.elasticsearch.painless.ContextDocGenerator'
-  classpath = sourceSets.doc.runtimeClasspath
-  systemProperty "cluster.uri", "${-> project.ext.generateContextNodes.collect { it.httpUri() }.join(',') }"
+task generateContextDoc(type: DefaultTestClustersTask) {
+  useCluster  testClusters.generateContextCluster
+  doFirst {
+    project.javaexec {
+      main = 'org.elasticsearch.painless.ContextDocGenerator'
+      classpath = sourceSets.doc.runtimeClasspath
+      systemProperty "cluster.uri", "${-> testClusters.generateContextCluster.singleNode().getAllHttpSocketURI()}"
+    }.assertNormalExitValue()
+  }
 }
 
 /**********************************************

+ 0 - 5
modules/rank-eval/build.gradle

@@ -26,8 +26,3 @@ testClusters.integTest {
   // Modules who's integration is explicitly tested in integration tests
   module file(project(':modules:lang-mustache').tasks.bundlePlugin.archiveFile)
 }
-
-run {
-  // Modules who's integration is explicitly tested in integration tests
-  module project(':modules:lang-mustache')
-}

+ 0 - 8
modules/reindex/build.gradle

@@ -36,14 +36,6 @@ testClusters.integTest {
   setting 'reindex.remote.whitelist', '127.0.0.1:*'
 }
 
-run {
-  // Modules who's integration is explicitly tested in integration tests
-  module project(':modules:parent-join')
-  module project(':modules:lang-painless')
-  // Whitelist reindexing from the local node so we can test reindex-from-remote.
-  setting 'reindex.remote.whitelist', '127.0.0.1:*'
-}
-
 test {
   /*
    * We have to disable setting the number of available processors as tests in the

+ 0 - 2
x-pack/docs/build.gradle

@@ -1,5 +1,3 @@
-import org.elasticsearch.gradle.test.NodeInfo
-
 import java.nio.charset.StandardCharsets
 
 apply plugin: 'elasticsearch.docs-test'

+ 0 - 1
x-pack/plugin/sql/qa/security/with-ssl/build.gradle

@@ -1,6 +1,5 @@
 import org.elasticsearch.gradle.BuildPlugin
 import org.elasticsearch.gradle.LoggedExec
-import org.elasticsearch.gradle.test.NodeInfo
 
 import javax.net.ssl.HttpsURLConnection
 import javax.net.ssl.KeyManagerFactory

+ 1 - 1
x-pack/qa/smoke-test-plugins-ssl/build.gradle

@@ -1,5 +1,5 @@
 import org.elasticsearch.gradle.MavenFilteringHack
-import org.elasticsearch.gradle.test.NodeInfo
+
 import org.elasticsearch.gradle.http.WaitForHttpResource
 
 apply plugin: 'elasticsearch.testclusters'