diff --git a/test/external-modules/multi-project/src/javaRestTest/java/org/elasticsearch/multiproject/action/DeleteProjectActionIT.java b/test/external-modules/multi-project/src/javaRestTest/java/org/elasticsearch/multiproject/action/DeleteProjectActionIT.java deleted file mode 100644 index 7d5fc5eeb1cf5..0000000000000 --- a/test/external-modules/multi-project/src/javaRestTest/java/org/elasticsearch/multiproject/action/DeleteProjectActionIT.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -package org.elasticsearch.multiproject.action; - -import org.elasticsearch.client.Request; -import org.elasticsearch.test.cluster.ElasticsearchCluster; -import org.elasticsearch.test.cluster.local.distribution.DistributionType; -import org.elasticsearch.test.rest.ESRestTestCase; -import org.junit.ClassRule; - -public class DeleteProjectActionIT extends ESRestTestCase { - - @ClassRule - public static ElasticsearchCluster CLUSTER = ElasticsearchCluster.local() - .distribution(DistributionType.DEFAULT) - .setting("test.multi_project.enabled", "true") - .setting("xpack.security.enabled", "false") - .build(); - - @Override - protected String getTestRestCluster() { - return CLUSTER.getHttpAddresses(); - } - - public void testDeleteProject() throws Exception { - client().performRequest(new Request("PUT", "/_project/foo")); - var response = client().performRequest(new Request("DELETE", "/_project/foo")); - assertOK(response); - assertAcknowledged(response); - // TODO: this should assert that the project is actually deleted from the metadata and routing table once the cluster state action - // is updated. - } -} diff --git a/test/external-modules/multi-project/src/javaRestTest/java/org/elasticsearch/multiproject/action/ProjectCrudActionIT.java b/test/external-modules/multi-project/src/javaRestTest/java/org/elasticsearch/multiproject/action/ProjectCrudActionIT.java new file mode 100644 index 0000000000000..a4da635473594 --- /dev/null +++ b/test/external-modules/multi-project/src/javaRestTest/java/org/elasticsearch/multiproject/action/ProjectCrudActionIT.java @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.multiproject.action; + +import org.elasticsearch.client.Request; +import org.elasticsearch.client.Response; +import org.elasticsearch.client.ResponseException; +import org.elasticsearch.test.cluster.ElasticsearchCluster; +import org.elasticsearch.test.cluster.local.distribution.DistributionType; +import org.elasticsearch.test.rest.ESRestTestCase; +import org.elasticsearch.test.rest.ObjectPath; +import org.junit.ClassRule; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.CoreMatchers.not; + +public class ProjectCrudActionIT extends ESRestTestCase { + + @ClassRule + public static ElasticsearchCluster CLUSTER = ElasticsearchCluster.local() + .distribution(DistributionType.DEFAULT) + .setting("test.multi_project.enabled", "true") + .setting("xpack.security.enabled", "false") + .build(); + + @Override + protected String getTestRestCluster() { + return CLUSTER.getHttpAddresses(); + } + + public void testCreateAndDeleteProject() throws IOException { + final var projectId = randomUniqueProjectId(); + var request = new Request("PUT", "/_project/" + projectId); + + final int numberOfRequests = between(1, 8); + final var successCount = new AtomicInteger(); + final var errorCount = new AtomicInteger(); + + runInParallel(numberOfRequests, ignore -> { + try { + var response = client().performRequest(request); + assertAcknowledged(response); + successCount.incrementAndGet(); + } catch (IOException e) { + if (e instanceof ResponseException responseException) { + assertThat(responseException.getMessage(), containsString("project [" + projectId + "] already exists")); + errorCount.incrementAndGet(); + return; + } + fail(e, "unexpected exception"); + } + }); + + assertThat(successCount.get(), equalTo(1)); + assertThat(errorCount.get(), equalTo(numberOfRequests - 1)); + assertThat(getProjectIdsFromClusterState(), hasItem(projectId.id())); + + final Response response = client().performRequest(new Request("DELETE", "/_project/" + projectId)); + assertAcknowledged(response); + assertThat(getProjectIdsFromClusterState(), not(hasItem(projectId.id()))); + } + + private Set getProjectIdsFromClusterState() throws IOException { + final Response response = client().performRequest(new Request("GET", "/_cluster/state?multi_project=true")); + final ObjectPath clusterState = assertOKAndCreateObjectPath(response); + + final Set projectIdsFromMetadata = extractProjectIds(clusterState, "metadata.projects"); + final Set projectIdsFromRoutingTable = extractProjectIds(clusterState, "routing_table.projects"); + + assertThat(projectIdsFromMetadata, equalTo(projectIdsFromRoutingTable)); + return projectIdsFromMetadata; + } + + @SuppressWarnings("unchecked") + private Set extractProjectIds(ObjectPath clusterState, String path) throws IOException { + final int numberProjects = ((List) clusterState.evaluate(path)).size(); + return IntStream.range(0, numberProjects).mapToObj(i -> { + try { + return (String) clusterState.evaluate(path + "." + i + ".id"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }).collect(Collectors.toUnmodifiableSet()); + } +} diff --git a/test/external-modules/multi-project/src/main/java/org/elasticsearch/multiproject/action/PutProjectAction.java b/test/external-modules/multi-project/src/main/java/org/elasticsearch/multiproject/action/PutProjectAction.java index b6a799d4ce0e7..177eb0077d10a 100644 --- a/test/external-modules/multi-project/src/main/java/org/elasticsearch/multiproject/action/PutProjectAction.java +++ b/test/external-modules/multi-project/src/main/java/org/elasticsearch/multiproject/action/PutProjectAction.java @@ -9,6 +9,7 @@ package org.elasticsearch.multiproject.action; +import org.elasticsearch.ResourceAlreadyExistsException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.ActionType; @@ -37,6 +38,8 @@ import org.elasticsearch.transport.TransportService; import java.io.IOException; +import java.util.HashSet; +import java.util.Set; import java.util.regex.Pattern; public class PutProjectAction extends ActionType { @@ -105,11 +108,17 @@ static class PutProjectExecutor implements ClusterStateTaskExecutor batchExecutionContext) throws Exception { - var stateBuilder = ClusterState.builder(batchExecutionContext.initialState()); + final ClusterState initialState = batchExecutionContext.initialState(); + final Set knownProjectIds = new HashSet<>(initialState.metadata().projects().keySet()); + var stateBuilder = ClusterState.builder(initialState); for (TaskContext taskContext : batchExecutionContext.taskContexts()) { try { Request request = taskContext.getTask().request(); + if (knownProjectIds.contains(request.projectId)) { + throw new ResourceAlreadyExistsException("project [{}] already exists", request.projectId); + } stateBuilder.putProjectMetadata(ProjectMetadata.builder(request.projectId)); + knownProjectIds.add(request.projectId); taskContext.success(() -> taskContext.getTask().listener.onResponse(AcknowledgedResponse.TRUE)); } catch (Exception e) { taskContext.onFailure(e);