diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle
index 5775b2b6323f1..94f7da08e6093 100644
--- a/buildSrc/build.gradle
+++ b/buildSrc/build.gradle
@@ -23,6 +23,15 @@ plugins {
id 'groovy'
}
+gradlePlugin {
+ plugins {
+ simplePlugin {
+ id = 'elasticsearch.clusterformation'
+ implementationClass = 'org.elasticsearch.gradle.clusterformation.ClusterformationPlugin'
+ }
+ }
+}
+
group = 'org.elasticsearch.gradle'
String minimumGradleVersion = file('src/main/resources/minimumGradleVersion').text.trim()
diff --git a/buildSrc/src/main/java/org/elasticsearch/GradleServicesAdapter.java b/buildSrc/src/main/java/org/elasticsearch/GradleServicesAdapter.java
new file mode 100644
index 0000000000000..6d256ba044971
--- /dev/null
+++ b/buildSrc/src/main/java/org/elasticsearch/GradleServicesAdapter.java
@@ -0,0 +1,68 @@
+/*
+ * 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;
+
+import org.gradle.api.Action;
+import org.gradle.api.Project;
+import org.gradle.api.file.CopySpec;
+import org.gradle.api.file.FileTree;
+import org.gradle.api.tasks.WorkResult;
+import org.gradle.process.ExecResult;
+import org.gradle.process.JavaExecSpec;
+
+import java.io.File;
+
+/**
+ * Facilitate access to Gradle services without a direct dependency on Project.
+ *
+ * In a future release Gradle will offer service injection, this adapter plays that role until that time.
+ * It exposes the service methods that are part of the public API as the classes implementing them are not.
+ * Today service injection is not available for
+ * extensions.
+ *
+ * Everything exposed here must be thread safe. That is the very reason why project is not passed in directly.
+ */
+public class GradleServicesAdapter {
+
+ public final Project project;
+
+ public GradleServicesAdapter(Project project) {
+ this.project = project;
+ }
+
+ public static GradleServicesAdapter getInstance(Project project) {
+ return new GradleServicesAdapter(project);
+ }
+
+ public WorkResult copy(Action super CopySpec> action) {
+ return project.copy(action);
+ }
+
+ public WorkResult sync(Action super CopySpec> action) {
+ return project.sync(action);
+ }
+
+ public ExecResult javaexec(Action super JavaExecSpec> action) {
+ return project.javaexec(action);
+ }
+
+ public FileTree zipTree(File zipPath) {
+ return project.zipTree(zipPath);
+ }
+}
diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/Distribution.java b/buildSrc/src/main/java/org/elasticsearch/gradle/Distribution.java
new file mode 100644
index 0000000000000..c926e70b3f765
--- /dev/null
+++ b/buildSrc/src/main/java/org/elasticsearch/gradle/Distribution.java
@@ -0,0 +1,36 @@
+/*
+ * 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;
+
+public enum Distribution {
+
+ INTEG_TEST("integ-test-zip"),
+ ZIP("zip"),
+ ZIP_OSS("zip-oss");
+
+ private final String name;
+
+ Distribution(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+}
diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/clusterformation/ClusterFormationTaskExecutionListener.java b/buildSrc/src/main/java/org/elasticsearch/gradle/clusterformation/ClusterFormationTaskExecutionListener.java
new file mode 100644
index 0000000000000..e4a50d77599e4
--- /dev/null
+++ b/buildSrc/src/main/java/org/elasticsearch/gradle/clusterformation/ClusterFormationTaskExecutionListener.java
@@ -0,0 +1,47 @@
+/*
+ * 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.clusterformation;
+
+import org.gradle.api.Task;
+import org.gradle.api.execution.TaskActionListener;
+import org.gradle.api.execution.TaskExecutionListener;
+import org.gradle.api.tasks.TaskState;
+
+public class ClusterFormationTaskExecutionListener implements TaskExecutionListener, TaskActionListener {
+ @Override
+ public void afterExecute(Task task, TaskState state) {
+ // always unclaim the cluster, even if _this_ task is up-to-date, as others might not have been and caused the
+ // cluster to start.
+ ClusterFormationTaskExtension.getForTask(task).getClaimedClusters().forEach(ElasticsearchConfiguration::unClaimAndStop);
+ }
+
+ @Override
+ public void beforeActions(Task task) {
+ // we only start the cluster before the actions, so we'll not start it if the task is up-to-date
+ ClusterFormationTaskExtension.getForTask(task).getClaimedClusters().forEach(ElasticsearchConfiguration::start);
+ }
+
+ @Override
+ public void beforeExecute(Task task) {
+ }
+
+ @Override
+ public void afterActions(Task task) {
+ }
+}
diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/clusterformation/ClusterFormationTaskExtension.java b/buildSrc/src/main/java/org/elasticsearch/gradle/clusterformation/ClusterFormationTaskExtension.java
new file mode 100644
index 0000000000000..0cafd2d4bebef
--- /dev/null
+++ b/buildSrc/src/main/java/org/elasticsearch/gradle/clusterformation/ClusterFormationTaskExtension.java
@@ -0,0 +1,64 @@
+/*
+ * 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.clusterformation;
+
+import org.gradle.api.Task;
+import org.gradle.api.logging.Logger;
+import org.gradle.api.logging.Logging;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class ClusterFormationTaskExtension {
+
+ private final Task task;
+
+ private final List claimedClusters = new ArrayList<>();
+
+ private final Logger logger = Logging.getLogger(ClusterFormationTaskExtension.class);
+
+ public ClusterFormationTaskExtension(Task task) {
+ this.task = task;
+ }
+
+ public void call(ElasticsearchConfiguration cluster) {
+ // not likely to configure the same task from multiple threads as of Gradle 4.7, but it's the right thing to do
+ synchronized (claimedClusters) {
+ if (claimedClusters.contains(cluster)) {
+ logger.warn("{} already claimed cluster {} will not claim it again",
+ task.getPath(), cluster.getName()
+ );
+ return;
+ }
+ claimedClusters.add(cluster);
+ }
+ logger.info("CF: the {} task will use cluster: {}", task.getName(), cluster.getName());
+ }
+
+ public List getClaimedClusters() {
+ synchronized (claimedClusters) {
+ return Collections.unmodifiableList(claimedClusters);
+ }
+ }
+
+ static ClusterFormationTaskExtension getForTask(Task task) {
+ return task.getExtensions().getByType(ClusterFormationTaskExtension.class);
+ }
+}
diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/clusterformation/ClusterformationPlugin.java b/buildSrc/src/main/java/org/elasticsearch/gradle/clusterformation/ClusterformationPlugin.java
new file mode 100644
index 0000000000000..e79f67c0fd539
--- /dev/null
+++ b/buildSrc/src/main/java/org/elasticsearch/gradle/clusterformation/ClusterformationPlugin.java
@@ -0,0 +1,89 @@
+/*
+ * 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.clusterformation;
+
+import groovy.lang.Closure;
+import org.elasticsearch.GradleServicesAdapter;
+import org.gradle.api.NamedDomainObjectContainer;
+import org.gradle.api.Plugin;
+import org.gradle.api.Project;
+import org.gradle.api.Task;
+import org.gradle.api.logging.Logger;
+import org.gradle.api.logging.Logging;
+import org.gradle.api.plugins.ExtraPropertiesExtension;
+
+public class ClusterformationPlugin implements Plugin {
+
+ public static final String LIST_TASK_NAME = "listElasticSearchClusters";
+ public static final String EXTENSION_NAME = "elasticSearchClusters";
+ public static final String TASK_EXTENSION_NAME = "useClusterExt";
+
+ private final Logger logger = Logging.getLogger(ClusterformationPlugin.class);
+
+ @Override
+ public void apply(Project project) {
+ NamedDomainObjectContainer extends ElasticsearchConfiguration> container = project.container(
+ ElasticsearchNode.class,
+ (name) -> new ElasticsearchNode(name, GradleServicesAdapter.getInstance(project))
+ );
+ project.getExtensions().add(EXTENSION_NAME, container);
+
+ Task listTask = project.getTasks().create(LIST_TASK_NAME);
+ listTask.setGroup("ES cluster formation");
+ listTask.setDescription("Lists all ES clusters configured for this project");
+ listTask.doLast((Task task) ->
+ container.forEach((ElasticsearchConfiguration cluster) ->
+ logger.lifecycle(" * {}: {}", cluster.getName(), cluster.getDistribution())
+ )
+ );
+
+ // register an extension for all current and future tasks, so that any task can declare that it wants to use a
+ // specific cluster.
+ project.getTasks().all((Task task) -> {
+ ClusterFormationTaskExtension taskExtension = task.getExtensions().create(
+ TASK_EXTENSION_NAME, ClusterFormationTaskExtension.class, task
+ );
+ // Gradle doesn't look for extensions that might implement `call` and instead only looks for task methods
+ // work around by creating a closure in the extra properties extensions which does work
+ task.getExtensions().findByType(ExtraPropertiesExtension.class)
+ .set(
+ "useCluster",
+ new Closure(this, this) {
+ public void doCall(ElasticsearchConfiguration conf) {
+ taskExtension.call(conf);
+ }
+ }
+ );
+ });
+
+ // Make sure we only claim the clusters for the tasks that will actually execute
+ project.getGradle().getTaskGraph().whenReady(taskExecutionGraph ->
+ taskExecutionGraph.getAllTasks().forEach(
+ task -> ClusterFormationTaskExtension.getForTask(task).getClaimedClusters().forEach(
+ ElasticsearchConfiguration::claim
+ )
+ )
+ );
+
+ // create the listener to start the clusters on-demand and terminate when no longer claimed.
+ // we need to use a task execution listener, as tasl
+ project.getGradle().addListener(new ClusterFormationTaskExecutionListener());
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/clusterformation/ElasticsearchConfiguration.java b/buildSrc/src/main/java/org/elasticsearch/gradle/clusterformation/ElasticsearchConfiguration.java
new file mode 100644
index 0000000000000..913d88e9fa11b
--- /dev/null
+++ b/buildSrc/src/main/java/org/elasticsearch/gradle/clusterformation/ElasticsearchConfiguration.java
@@ -0,0 +1,46 @@
+/*
+ * 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.clusterformation;
+
+import org.elasticsearch.gradle.Distribution;
+import org.elasticsearch.gradle.Version;
+
+import java.util.concurrent.Future;
+
+public interface ElasticsearchConfiguration {
+ String getName();
+
+ Version getVersion();
+
+ void setVersion(Version version);
+
+ default void setVersion(String version) {
+ setVersion(Version.fromString(version));
+ }
+
+ Distribution getDistribution();
+
+ void setDistribution(Distribution distribution);
+
+ void claim();
+
+ Future start();
+
+ void unClaimAndStop();
+}
diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/clusterformation/ElasticsearchNode.java b/buildSrc/src/main/java/org/elasticsearch/gradle/clusterformation/ElasticsearchNode.java
new file mode 100644
index 0000000000000..8b78fc2b627cb
--- /dev/null
+++ b/buildSrc/src/main/java/org/elasticsearch/gradle/clusterformation/ElasticsearchNode.java
@@ -0,0 +1,130 @@
+/*
+ * 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.clusterformation;
+
+import org.elasticsearch.GradleServicesAdapter;
+import org.elasticsearch.gradle.Distribution;
+import org.elasticsearch.gradle.Version;
+import org.gradle.api.logging.Logger;
+import org.gradle.api.logging.Logging;
+
+import java.util.Objects;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class ElasticsearchNode implements ElasticsearchConfiguration {
+
+ private final String name;
+ private final GradleServicesAdapter services;
+ private final AtomicInteger noOfClaims = new AtomicInteger();
+ private final AtomicBoolean started = new AtomicBoolean(false);
+ private final Logger logger = Logging.getLogger(ElasticsearchNode.class);
+
+ private Distribution distribution;
+ private Version version;
+
+ public ElasticsearchNode(String name, GradleServicesAdapter services) {
+ this.name = name;
+ this.services = services;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public Version getVersion() {
+ return version;
+ }
+
+ @Override
+ public void setVersion(Version version) {
+ checkNotRunning();
+ this.version = version;
+ }
+
+ @Override
+ public Distribution getDistribution() {
+ return distribution;
+ }
+
+ @Override
+ public void setDistribution(Distribution distribution) {
+ checkNotRunning();
+ this.distribution = distribution;
+ }
+
+ @Override
+ public void claim() {
+ noOfClaims.incrementAndGet();
+ }
+
+ /**
+ * Start the cluster if not running. Does nothing if the cluster is already running.
+ *
+ * @return future of thread running in the background
+ */
+ @Override
+ public Future start() {
+ if (started.getAndSet(true)) {
+ logger.lifecycle("Already started cluster: {}", name);
+ } else {
+ logger.lifecycle("Starting cluster: {}", name);
+ }
+ return null;
+ }
+
+ /**
+ * Stops a running cluster if it's not claimed. Does nothing otherwise.
+ */
+ @Override
+ public void unClaimAndStop() {
+ int decrementedClaims = noOfClaims.decrementAndGet();
+ if (decrementedClaims > 0) {
+ logger.lifecycle("Not stopping {}, since cluster still has {} claim(s)", name, decrementedClaims);
+ return;
+ }
+ if (started.get() == false) {
+ logger.lifecycle("Asked to unClaimAndStop, but cluster was not running: {}", name);
+ return;
+ }
+ logger.lifecycle("Stopping {}, number of claims is {}", name, decrementedClaims);
+ }
+
+ private void checkNotRunning() {
+ if (started.get()) {
+ throw new IllegalStateException("Configuration can not be altered while running ");
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ElasticsearchNode that = (ElasticsearchNode) o;
+ return Objects.equals(name, that.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name);
+ }
+}
diff --git a/buildSrc/src/test/java/org/elasticsearch/gradle/clusterformation/ClusterformationPluginIT.java b/buildSrc/src/test/java/org/elasticsearch/gradle/clusterformation/ClusterformationPluginIT.java
new file mode 100644
index 0000000000000..c690557537dfb
--- /dev/null
+++ b/buildSrc/src/test/java/org/elasticsearch/gradle/clusterformation/ClusterformationPluginIT.java
@@ -0,0 +1,144 @@
+/*
+ * 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.clusterformation;
+
+import org.elasticsearch.gradle.test.GradleIntegrationTestCase;
+import org.gradle.testkit.runner.BuildResult;
+import org.gradle.testkit.runner.GradleRunner;
+import org.gradle.testkit.runner.TaskOutcome;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+public class ClusterformationPluginIT extends GradleIntegrationTestCase {
+
+ public void testListClusters() {
+ BuildResult result = GradleRunner.create()
+ .withProjectDir(getProjectDir("clusterformation"))
+ .withArguments("listElasticSearchClusters", "-s")
+ .withPluginClasspath()
+ .build();
+
+ assertEquals(TaskOutcome.SUCCESS, result.task(":listElasticSearchClusters").getOutcome());
+ assertOutputContains(
+ result.getOutput(),
+ " * myTestCluster:"
+ );
+
+ }
+
+ public void testUseClusterByOne() {
+ BuildResult result = GradleRunner.create()
+ .withProjectDir(getProjectDir("clusterformation"))
+ .withArguments("user1", "-s")
+ .withPluginClasspath()
+ .build();
+
+ assertEquals(TaskOutcome.SUCCESS, result.task(":user1").getOutcome());
+ assertOutputContains(
+ result.getOutput(),
+ "Starting cluster: myTestCluster",
+ "Stopping myTestCluster, number of claims is 0"
+ );
+ }
+
+ public void testUseClusterByOneWithDryRun() {
+ BuildResult result = GradleRunner.create()
+ .withProjectDir(getProjectDir("clusterformation"))
+ .withArguments("user1", "-s", "--dry-run")
+ .withPluginClasspath()
+ .build();
+
+ assertNull(result.task(":user1"));
+ assertOutputDoesNotContain(
+ result.getOutput(),
+ "Starting cluster: myTestCluster",
+ "Stopping myTestCluster, number of claims is 0"
+ );
+ }
+
+ public void testUseClusterByTwo() {
+ BuildResult result = GradleRunner.create()
+ .withProjectDir(getProjectDir("clusterformation"))
+ .withArguments("user1", "user2", "-s")
+ .withPluginClasspath()
+ .build();
+
+ assertEquals(TaskOutcome.SUCCESS, result.task(":user1").getOutcome());
+ assertEquals(TaskOutcome.SUCCESS, result.task(":user2").getOutcome());
+ assertOutputContains(
+ result.getOutput(),
+ "Starting cluster: myTestCluster",
+ "Not stopping myTestCluster, since cluster still has 1 claim(s)",
+ "Stopping myTestCluster, number of claims is 0"
+ );
+ }
+
+ public void testUseClusterByUpToDateTask() {
+ BuildResult result = GradleRunner.create()
+ .withProjectDir(getProjectDir("clusterformation"))
+ .withArguments("upToDate1", "upToDate2", "-s")
+ .withPluginClasspath()
+ .build();
+
+ assertEquals(TaskOutcome.UP_TO_DATE, result.task(":upToDate1").getOutcome());
+ assertEquals(TaskOutcome.UP_TO_DATE, result.task(":upToDate2").getOutcome());
+ assertOutputContains(
+ result.getOutput(),
+ "Not stopping myTestCluster, since cluster still has 1 claim(s)",
+ "cluster was not running: myTestCluster"
+ );
+ assertOutputDoesNotContain(result.getOutput(), "Starting cluster: myTestCluster");
+ }
+
+ public void testUseClusterBySkippedTask() {
+ BuildResult result = GradleRunner.create()
+ .withProjectDir(getProjectDir("clusterformation"))
+ .withArguments("skipped1", "skipped2", "-s")
+ .withPluginClasspath()
+ .build();
+
+ assertEquals(TaskOutcome.SKIPPED, result.task(":skipped1").getOutcome());
+ assertEquals(TaskOutcome.SKIPPED, result.task(":skipped2").getOutcome());
+ assertOutputContains(
+ result.getOutput(),
+ "Not stopping myTestCluster, since cluster still has 1 claim(s)",
+ "cluster was not running: myTestCluster"
+ );
+ assertOutputDoesNotContain(result.getOutput(), "Starting cluster: myTestCluster");
+ }
+
+ public void tetUseClusterBySkippedAndWorkingTask() {
+ BuildResult result = GradleRunner.create()
+ .withProjectDir(getProjectDir("clusterformation"))
+ .withArguments("skipped1", "user1", "-s")
+ .withPluginClasspath()
+ .build();
+
+ assertEquals(TaskOutcome.SKIPPED, result.task(":skipped1").getOutcome());
+ assertEquals(TaskOutcome.SUCCESS, result.task(":user1").getOutcome());
+ assertOutputContains(
+ result.getOutput(),
+ "> Task :user1",
+ "Starting cluster: myTestCluster",
+ "Stopping myTestCluster, number of claims is 0"
+ );
+ }
+
+}
diff --git a/buildSrc/src/testKit/clusterformation/build.gradle b/buildSrc/src/testKit/clusterformation/build.gradle
new file mode 100644
index 0000000000000..ae9dd8a2c335c
--- /dev/null
+++ b/buildSrc/src/testKit/clusterformation/build.gradle
@@ -0,0 +1,41 @@
+plugins {
+ id 'elasticsearch.clusterformation'
+}
+
+elasticSearchClusters {
+ myTestCluster {
+ distribution = 'ZIP'
+ }
+}
+
+task user1 {
+ useCluster elasticSearchClusters.myTestCluster
+ doLast {
+ println "user1 executing"
+ }
+}
+
+task user2 {
+ useCluster elasticSearchClusters.myTestCluster
+ doLast {
+ println "user2 executing"
+ }
+}
+
+task upToDate1 {
+ useCluster elasticSearchClusters.myTestCluster
+}
+
+task upToDate2 {
+ useCluster elasticSearchClusters.myTestCluster
+}
+
+task skipped1 {
+ enabled = false
+ useCluster elasticSearchClusters.myTestCluster
+}
+
+task skipped2 {
+ enabled = false
+ useCluster elasticSearchClusters.myTestCluster
+}