diff --git a/server/src/main/java/org/elasticsearch/TransportVersions.java b/server/src/main/java/org/elasticsearch/TransportVersions.java index 85929fd52b864..8bf8a94fccfe0 100644 --- a/server/src/main/java/org/elasticsearch/TransportVersions.java +++ b/server/src/main/java/org/elasticsearch/TransportVersions.java @@ -297,6 +297,7 @@ static TransportVersion def(int id) { public static final TransportVersion SEARCH_LOAD_PER_INDEX_STATS = def(9_095_0_00); public static final TransportVersion HEAP_USAGE_IN_CLUSTER_INFO = def(9_096_0_00); public static final TransportVersion NONE_CHUNKING_STRATEGY = def(9_097_0_00); + public static final TransportVersion PROJECT_DELETION_GLOBAL_BLOCK = def(9_098_0_00); /* * STOP! READ THIS FIRST! No, really, diff --git a/server/src/main/java/org/elasticsearch/cluster/ClusterState.java b/server/src/main/java/org/elasticsearch/cluster/ClusterState.java index 8de44623b9db6..fdc74d6e354c5 100644 --- a/server/src/main/java/org/elasticsearch/cluster/ClusterState.java +++ b/server/src/main/java/org/elasticsearch/cluster/ClusterState.java @@ -873,13 +873,13 @@ private Iterator blocksXContentMultiProjects() { } builder.endObject(); } - if (blocks().noIndexBlockAllProjects() == false) { + if (blocks().noProjectHasAProjectBlock() == false) { builder.startArray("projects"); } return builder; }; final ToXContent after = (builder, params) -> { - if (blocks().noIndexBlockAllProjects() == false) { + if (blocks().noProjectHasAProjectBlock() == false) { builder.endArray(); } return builder.endObject(); @@ -887,7 +887,10 @@ private Iterator blocksXContentMultiProjects() { return chunkedSection( true, before, - Iterators.map(metadata().projects().keySet().iterator(), projectId -> new Tuple<>(projectId, blocks().indices(projectId))), + Iterators.map( + metadata().projects().keySet().iterator(), + projectId -> new Tuple<>(projectId, blocks().projectBlocks(projectId)) + ), ClusterState::projectBlocksXContent, after ); @@ -929,19 +932,37 @@ private Iterator blocksXContentSingleProject(ProjectId singleProject ); } - private static Iterator projectBlocksXContent(Tuple>> entry) { - return chunkedSection( - entry.v2().isEmpty() == false, - (builder, params) -> builder.startObject().field("id", entry.v1()).startObject("indices"), - entry.v2().entrySet().iterator(), - e -> Iterators.single((builder, params) -> { - builder.startObject(e.getKey()); - for (ClusterBlock block : e.getValue()) { + private static Iterator projectBlocksXContent(Tuple entry) { + final var projectId = entry.v1(); + final var projectBlocks = entry.v2(); + if (projectBlocks.isEmpty()) { + return Collections.emptyIterator(); + } + return Iterators.concat( + Iterators.single((builder, params) -> builder.startObject().field("id", projectId)), + // write project global blocks in one chunk + projectBlocks.projectGlobals().isEmpty() ? Collections.emptyIterator() : Iterators.single((builder, params) -> { + builder.startObject("project_globals"); + for (ClusterBlock block : projectBlocks.projectGlobals()) { block.toXContent(builder, params); } return builder.endObject(); }), - (builder, params) -> builder.endObject().endObject() + // write index blocks for the project + projectBlocks.indices().isEmpty() + ? Collections.emptyIterator() + : Iterators.concat( + Iterators.single((builder, params) -> builder.startObject("indices")), + Iterators.flatMap(projectBlocks.indices().entrySet().iterator(), indexBlocks -> Iterators.single((builder, params) -> { + builder.startObject(indexBlocks.getKey()); + for (ClusterBlock block : indexBlocks.getValue()) { + block.toXContent(builder, params); + } + return builder.endObject(); + })), + Iterators.single((builder, params) -> builder.endObject()) + ), + Iterators.single((builder, params) -> builder.endObject()) ); } diff --git a/server/src/main/java/org/elasticsearch/cluster/block/ClusterBlocks.java b/server/src/main/java/org/elasticsearch/cluster/block/ClusterBlocks.java index bbcd816410a45..736f7f9379efd 100644 --- a/server/src/main/java/org/elasticsearch/cluster/block/ClusterBlocks.java +++ b/server/src/main/java/org/elasticsearch/cluster/block/ClusterBlocks.java @@ -72,18 +72,35 @@ public Set global() { return global; } - public boolean noIndexBlockAllProjects() { + public Set global(ProjectId projectId) { + return Sets.union(global, projectBlocks(projectId).projectGlobals()); + } + + public boolean noProjectHasAProjectBlock() { return projectBlocksMap.values().stream().allMatch(ProjectBlocks::isEmpty); } public Map> indices(ProjectId projectId) { - return projectBlocksMap.getOrDefault(projectId, ProjectBlocks.EMPTY).indices(); + return projectBlocks(projectId).indices(); + } + + public ProjectBlocks projectBlocks(ProjectId projectId) { + return projectBlocksMap.getOrDefault(projectId, ProjectBlocks.EMPTY); + } + + protected Set projectGlobal(ProjectId projectId) { + return projectBlocks(projectId).projectGlobals(); } public Set global(ClusterBlockLevel level) { return levelHolders.get(level).global(); } + public Set global(ProjectId projectId, ClusterBlockLevel level) { + var levelHolder = levelHolders.get(level); + return Sets.union(levelHolder.global, levelHolder.projects.getOrDefault(projectId, ProjectBlocks.EMPTY).projectGlobals()); + } + public Map> indices(ProjectId projectId, ClusterBlockLevel level) { return levelHolders.get(level).projects.getOrDefault(projectId, ProjectBlocks.EMPTY).indices(); } @@ -109,7 +126,8 @@ private static EnumMap generateLevelHol for (Map.Entry> indexEntry : projectEntry.getValue().indices().entrySet()) { indicesBuilder.put(indexEntry.getKey(), addBlocksAtLevel(indexEntry.getValue(), scratch, level)); } - projectsBuilder.put(projectEntry.getKey(), new ProjectBlocks(Map.copyOf(indicesBuilder))); + var projectGlobals = addBlocksAtLevel(projectEntry.getValue().projectGlobals(), scratch, level); + projectsBuilder.put(projectEntry.getKey(), new ProjectBlocks(Map.copyOf(indicesBuilder), projectGlobals)); indicesBuilder.clear(); } levelHolders.put(level, new ImmutableLevelHolder(addBlocksAtLevel(global, scratch, level), Map.copyOf(projectsBuilder))); @@ -223,6 +241,10 @@ private boolean globalBlocked(ClusterBlockLevel level) { return global(level).isEmpty() == false; } + private boolean globalBlocked(ProjectId projectId, ClusterBlockLevel level) { + return global(projectId, level).isEmpty() == false; + } + public ClusterBlockException globalBlockedException(ClusterBlockLevel level) { if (globalBlocked(level) == false) { return null; @@ -255,7 +277,7 @@ public boolean indexBlocked(ClusterBlockLevel level, String index) { } public boolean indexBlocked(ProjectId projectId, ClusterBlockLevel level, String index) { - return globalBlocked(level) || blocksForIndex(projectId, level, index).isEmpty() == false; + return globalBlocked(projectId, level) || blocksForIndex(projectId, level, index).isEmpty() == false; } @Deprecated(forRemoval = true) @@ -265,7 +287,7 @@ public ClusterBlockException indicesBlockedException(ClusterBlockLevel level, St } public ClusterBlockException indicesBlockedException(ProjectId projectId, ClusterBlockLevel level, String[] indices) { - Set globalLevelBlocks = global(level); + Set globalLevelBlocks = global(projectId, level); Map> indexLevelBlocks = new HashMap<>(); for (String index : indices) { Set indexBlocks = blocksForIndex(projectId, level, index); @@ -283,14 +305,15 @@ public ClusterBlockException indicesBlockedException(ProjectId projectId, Cluste } /** - * Returns true iff non of the given have a {@link ClusterBlockLevel#METADATA_WRITE} in place where the + * Returns true iff none of the given indices have a {@link ClusterBlockLevel#METADATA_WRITE} in place where the * {@link ClusterBlock#isAllowReleaseResources()} returns false. This is used in places where resources will be released * like the deletion of an index to free up resources on nodes. + * * @param projectId the project that owns the indices - * @param indices the indices to check + * @param indices the indices to check */ public ClusterBlockException indicesAllowReleaseResources(ProjectId projectId, String[] indices) { - Set globalBlocks = global(ClusterBlockLevel.METADATA_WRITE).stream() + Set globalBlocks = global(projectId, ClusterBlockLevel.METADATA_WRITE).stream() .filter(clusterBlock -> clusterBlock.isAllowReleaseResources() == false) .collect(toSet()); Map> indexLevelBlocks = new HashMap<>(); @@ -314,7 +337,7 @@ public ClusterBlockException indicesAllowReleaseResources(ProjectId projectId, S @Override public String toString() { - if (global.isEmpty() && noIndexBlockAllProjects()) { + if (global.isEmpty() && noProjectHasAProjectBlock()) { return ""; } StringBuilder sb = new StringBuilder(); @@ -328,6 +351,12 @@ public String toString() { for (var projectId : projectBlocksMap.keySet().stream().sorted(Comparator.comparing(ProjectId::id)).toList()) { final Map> indices = indices(projectId); sb.append(" ").append(projectId).append(":\n"); + if (projectGlobal(projectId).isEmpty() == false) { + sb.append(" _project_global_:\n"); + } + for (ClusterBlock block : projectGlobal(projectId)) { + sb.append(" ").append(block).append("\n"); + } for (Map.Entry> entry : indices.entrySet()) { sb.append(" ").append(entry.getKey()).append(":\n"); for (ClusterBlock block : entry.getValue()) { @@ -345,7 +374,7 @@ public void writeTo(StreamOutput out) throws IOException { out.writeMap(projectBlocksMap, (o, projectId) -> projectId.writeTo(o), (o, projectBlocks) -> projectBlocks.writeTo(out)); } else { if (noProjectOrDefaultProjectOnly()) { - writeToBwc(out); + writeToSingleProjectNode(out); } else { throw new IllegalStateException( "Cannot write multi-project blocks to a stream with version [" + out.getTransportVersion() + "]" @@ -354,7 +383,7 @@ public void writeTo(StreamOutput out) throws IOException { } } - private void writeToBwc(StreamOutput out) throws IOException { + private void writeToSingleProjectNode(StreamOutput out) throws IOException { writeBlockSet(global, out); out.writeMap(indices(Metadata.DEFAULT_PROJECT_ID), (o, s) -> writeBlockSet(s, o)); } @@ -387,16 +416,16 @@ private void throwIfMultiProjects() { private static class ClusterBlocksDiff implements Diff { private final ClusterBlocks part; - private final boolean isFromBwcNode; + private final boolean isFromSingleProjectNode; - ClusterBlocksDiff(ClusterBlocks part, boolean isFromBwcNode) { + ClusterBlocksDiff(ClusterBlocks part, boolean isFromSingleProjectNode) { this.part = part; - this.isFromBwcNode = isFromBwcNode; + this.isFromSingleProjectNode = isFromSingleProjectNode; } @Override public ClusterBlocks apply(ClusterBlocks part) { - if (isFromBwcNode) { + if (isFromSingleProjectNode) { if (part.noProjectOrDefaultProjectOnly()) { return this.part; } else { @@ -421,7 +450,7 @@ public void writeTo(StreamOutput out) throws IOException { } else { if (part.noProjectOrDefaultProjectOnly()) { out.writeBoolean(true); - part.writeToBwc(out); + part.writeToSingleProjectNode(out); } else { throw new IllegalStateException( "Cannot write multi-project blocks diff to a stream with version [" + out.getTransportVersion() + "]" @@ -446,11 +475,11 @@ && noProjectOrDefaultProjectOnly(projectBlocksMap) } return new ClusterBlocks(global, projectBlocksMap); } else { - return readFromBwc(in); + return readFromSingleProjectNode(in); } } - private static ClusterBlocks readFromBwc(StreamInput in) throws IOException { + private static ClusterBlocks readFromSingleProjectNode(StreamInput in) throws IOException { final Set global = readBlockSet(in); Map> indicesBlocks = in.readImmutableMap(i -> i.readString().intern(), ClusterBlocks::readBlockSet); if (global.isEmpty() && indicesBlocks.isEmpty()) { @@ -459,7 +488,7 @@ private static ClusterBlocks readFromBwc(StreamInput in) throws IOException { if (indicesBlocks.isEmpty()) { return new ClusterBlocks(global, Map.of()); } - return new ClusterBlocks(global, Map.of(Metadata.DEFAULT_PROJECT_ID, new ProjectBlocks(indicesBlocks))); + return new ClusterBlocks(global, Map.of(Metadata.DEFAULT_PROJECT_ID, new ProjectBlocks(indicesBlocks, Set.of()))); } private static Set readBlockSet(StreamInput in) throws IOException { @@ -471,41 +500,63 @@ public static Diff readDiffFrom(StreamInput in) throws IOExceptio if (in.getTransportVersion().onOrAfter(TransportVersions.MULTI_PROJECT)) { return new ClusterBlocksDiff(ClusterBlocks.readFrom(in), false); } else { - return new ClusterBlocksDiff(ClusterBlocks.readFromBwc(in), true); + return new ClusterBlocksDiff(ClusterBlocks.readFromSingleProjectNode(in), true); } } return SimpleDiffable.empty(); } - static class ProjectBlocks implements Writeable { + /** + * ProjectBlocks encapsulates the project-specific ClusterBlocks. These apply either to a specific index + * or are project global blocks. Project global blocks are similar to cluster-global blocks, but impact + * only one project. + */ + public static class ProjectBlocks implements Writeable { - static final ProjectBlocks EMPTY = new ProjectBlocks(Map.of()); + static final ProjectBlocks EMPTY = new ProjectBlocks(Map.of(), Set.of()); private final Map> indices; - ProjectBlocks(Map> indices) { + private final Set projectGlobal; + + ProjectBlocks(Map> indices, Set projectGlobal) { this.indices = indices; + this.projectGlobal = projectGlobal; } - Map> indices() { + public Map> indices() { return indices; } + public Set projectGlobals() { + return projectGlobal; + } + Set get(String index) { return indices.get(index); } - boolean isEmpty() { - return indices.isEmpty(); + public boolean isEmpty() { + return indices.isEmpty() && projectGlobal.isEmpty(); } static ProjectBlocks readFrom(StreamInput in) throws IOException { - return new ProjectBlocks(in.readImmutableMap(i -> i.readString().intern(), ClusterBlocks::readBlockSet)); + Map> indices = in.readImmutableMap(i -> i.readString().intern(), ClusterBlocks::readBlockSet); + Set projectGlobal; + if (in.getTransportVersion().onOrAfter(TransportVersions.PROJECT_DELETION_GLOBAL_BLOCK)) { + projectGlobal = ClusterBlocks.readBlockSet(in); + } else { + projectGlobal = Set.of(); + } + return new ProjectBlocks(indices, projectGlobal); } @Override public void writeTo(StreamOutput out) throws IOException { out.writeMap(indices, (o, s) -> writeBlockSet(s, o)); + if (out.getTransportVersion().onOrAfter(TransportVersions.PROJECT_DELETION_GLOBAL_BLOCK)) { + writeBlockSet(projectGlobal, out); + } } } @@ -514,6 +565,7 @@ record ImmutableLevelHolder(Set global, Map global = new HashSet<>(); - - private final Map>> projects = new HashMap<>(); + private final Map projects = new HashMap<>(); public Builder() {} + private static ProjectBlocks emptyMutableProjectBlocks() { + return new ProjectBlocks(new HashMap<>(), new HashSet<>()); + } + public Builder blocks(ClusterBlocks blocks) { global.addAll(blocks.global()); for (var projectId : blocks.projectBlocksMap.keySet()) { - final var indices = projects.computeIfAbsent(projectId, k -> new HashMap<>()); + final var projectBlocks = projects.computeIfAbsent(projectId, k -> emptyMutableProjectBlocks()); + projectBlocks.projectGlobal.addAll(blocks.projectGlobal(projectId)); for (Map.Entry> entry : blocks.indices(projectId).entrySet()) { - if (indices.containsKey(entry.getKey()) == false) { - indices.put(entry.getKey(), new HashSet<>()); + if (projectBlocks.indices.containsKey(entry.getKey()) == false) { + projectBlocks.indices.put(entry.getKey(), new HashSet<>()); } - indices.get(entry.getKey()).addAll(entry.getValue()); + projectBlocks.indices.get(entry.getKey()).addAll(entry.getValue()); } } return this; @@ -596,7 +652,7 @@ public Builder updateBlocks(IndexMetadata indexMetadata) { public Builder updateBlocks(ProjectId projectId, IndexMetadata indexMetadata) { // let's remove all blocks for this index and add them back -- no need to remove all individual blocks.... - projects.computeIfAbsent(projectId, k -> new HashMap<>()).remove(indexMetadata.getIndex().getName()); + projects.computeIfAbsent(projectId, k -> emptyMutableProjectBlocks()).indices.remove(indexMetadata.getIndex().getName()); return addBlocks(projectId, indexMetadata); } @@ -620,17 +676,31 @@ public Builder removeProject(ProjectId projectId) { return this; } + public Builder addProjectGlobalBlock(ProjectId projectId, ClusterBlock block) { + assert projectId.equals(ProjectId.DEFAULT) == false; + projects.computeIfAbsent(projectId, k -> emptyMutableProjectBlocks()).projectGlobal.add(block); + return this; + } + + public Builder removeProjectGlobalBlock(ProjectId projectId, ClusterBlock block) { + var project = projects.get(projectId); + if (project != null) { + project.projectGlobal.remove(block); + } + return this; + } + @Deprecated(forRemoval = true) public Builder addIndexBlock(String index, ClusterBlock block) { return addIndexBlock(Metadata.DEFAULT_PROJECT_ID, index, block); } public Builder addIndexBlock(ProjectId projectId, String index, ClusterBlock block) { - final var indices = projects.computeIfAbsent(projectId, k -> new HashMap<>()); - if (indices.containsKey(index) == false) { - indices.put(index, new HashSet<>()); + final var projectBlocks = projects.computeIfAbsent(projectId, k -> emptyMutableProjectBlocks()); + if (projectBlocks.indices().containsKey(index) == false) { + projectBlocks.indices.put(index, new HashSet<>()); } - indices.get(index).add(block); + projectBlocks.indices.get(index).add(block); return this; } @@ -640,62 +710,62 @@ public Builder removeIndexBlocks(String index) { } public Builder removeIndexBlocks(ProjectId projectId, String index) { - final var indices = projects.get(projectId); - if (indices == null) { + final var projectBlocks = projects.get(projectId); + if (projectBlocks == null) { return this; } - if (indices.containsKey(index) == false) { + if (projectBlocks.indices.containsKey(index) == false) { return this; } - indices.remove(index); + projectBlocks.indices.remove(index); return this; } public boolean hasIndexBlock(ProjectId projectId, String index, ClusterBlock block) { - final var indices = projects.get(projectId); - if (indices == null) { + final var projectBlocks = projects.get(projectId); + if (projectBlocks == null) { return false; } - return indices.getOrDefault(index, Set.of()).contains(block); + return projectBlocks.indices.getOrDefault(index, Set.of()).contains(block); } public boolean hasIndexBlockLevel(ProjectId projectId, String index, ClusterBlockLevel level) { - final var indices = projects.get(projectId); - if (indices == null) { + final var projectBlocks = projects.get(projectId); + if (projectBlocks == null) { return false; } - return indices.getOrDefault(index, Set.of()).stream().anyMatch(clusterBlock -> clusterBlock.contains(level)); + return projectBlocks.indices.getOrDefault(index, Set.of()).stream().anyMatch(clusterBlock -> clusterBlock.contains(level)); } public Builder removeIndexBlock(ProjectId projectId, String index, ClusterBlock block) { - final var indices = projects.get(projectId); - if (indices == null) { + final var projectBlocks = projects.get(projectId); + if (projectBlocks == null) { return this; } - if (indices.containsKey(index) == false) { + if (projectBlocks.indices.containsKey(index) == false) { return this; } - indices.get(index).remove(block); - if (indices.get(index).isEmpty()) { - indices.remove(index); + projectBlocks.get(index).remove(block); + if (projectBlocks.get(index).isEmpty()) { + projectBlocks.indices.remove(index); } return this; } public Builder removeIndexBlockWithId(ProjectId projectId, String index, int blockId) { - final var indices = projects.get(projectId); - if (indices == null) { + final var projectBlocks = projects.get(projectId); + if (projectBlocks == null) { return this; } - final Set indexBlocks = indices.get(index); + final Set indexBlocks = projectBlocks.get(index); if (indexBlocks == null) { return this; } indexBlocks.removeIf(block -> block.id() == blockId); if (indexBlocks.isEmpty()) { - indices.remove(index); + projectBlocks.indices.remove(index); } return this; } @@ -703,18 +773,21 @@ public Builder removeIndexBlockWithId(ProjectId projectId, String index, int blo public ClusterBlocks build() { if (global.isEmpty() && noProjectOrDefaultProjectOnly(projects) - && projects.getOrDefault(Metadata.DEFAULT_PROJECT_ID, Map.of()).isEmpty()) { + && projects.getOrDefault(Metadata.DEFAULT_PROJECT_ID, ProjectBlocks.EMPTY).isEmpty()) { return EMPTY_CLUSTER_BLOCK; } // We copy the block sets here in case of the builder is modified after build is called Map projectsBuilder = new HashMap<>(projects.size()); - for (Map.Entry>> projectEntry : projects.entrySet()) { - Map> indicesBuilder = new HashMap<>(projectEntry.getValue().size()); - for (Map.Entry> indexEntry : projectEntry.getValue().entrySet()) { + for (Map.Entry projectEntry : projects.entrySet()) { + Map> indicesBuilder = new HashMap<>(projectEntry.getValue().indices.size()); + for (Map.Entry> indexEntry : projectEntry.getValue().indices.entrySet()) { indicesBuilder.put(indexEntry.getKey(), Set.copyOf(indexEntry.getValue())); } - if (indicesBuilder.isEmpty() == false) { - projectsBuilder.put(projectEntry.getKey(), new ProjectBlocks(Map.copyOf(indicesBuilder))); + if (indicesBuilder.isEmpty() == false || projectEntry.getValue().projectGlobals().isEmpty() == false) { + projectsBuilder.put( + projectEntry.getKey(), + new ProjectBlocks(Map.copyOf(indicesBuilder), Set.copyOf(projectEntry.getValue().projectGlobals())) + ); } } return new ClusterBlocks(Set.copyOf(global), Map.copyOf(projectsBuilder)); diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/ProjectMetadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/ProjectMetadata.java index 5584b05f1c43a..480172ef3aace 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/ProjectMetadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/ProjectMetadata.java @@ -15,6 +15,8 @@ import org.elasticsearch.cluster.Diffable; import org.elasticsearch.cluster.DiffableUtils; import org.elasticsearch.cluster.NamedDiffableValueSerializer; +import org.elasticsearch.cluster.block.ClusterBlock; +import org.elasticsearch.cluster.block.ClusterBlockLevel; import org.elasticsearch.cluster.routing.GlobalRoutingTable; import org.elasticsearch.cluster.routing.allocation.IndexMetadataUpdater; import org.elasticsearch.common.Strings; @@ -43,6 +45,7 @@ import org.elasticsearch.persistent.PersistentTasksCustomMetadata; import org.elasticsearch.plugins.FieldPredicate; import org.elasticsearch.plugins.MapperPlugin; +import org.elasticsearch.rest.RestStatus; import org.elasticsearch.transport.Transports; import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentParser; @@ -53,6 +56,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -106,6 +110,16 @@ public class ProjectMetadata implements Iterable, Diffable> indicesBlocks = clusterState.blocks().indices(projectId); if (indicesBlocks.isEmpty() == false) { chunkCount += 2 + indicesBlocks.size(); } + if (clusterState.blocks().projectBlocks(projectId).projectGlobals().isEmpty() == false) { + chunkCount += 1; + } } } } diff --git a/server/src/test/java/org/elasticsearch/cluster/block/ClusterBlockTests.java b/server/src/test/java/org/elasticsearch/cluster/block/ClusterBlockTests.java index 068b259548d59..6d398d4677f25 100644 --- a/server/src/test/java/org/elasticsearch/cluster/block/ClusterBlockTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/block/ClusterBlockTests.java @@ -171,6 +171,28 @@ public void testGetIndexBlockWithId() { ); } + public void testProjectGlobal() { + final ProjectId project1 = randomUniqueProjectId(); + final ProjectId project2 = randomUniqueProjectId(); + final ClusterBlocks.Builder builder = ClusterBlocks.builder(); + final var project1Index = randomIdentifier(); + final var indexBlock = randomClusterBlock(randomVersion()); + final var globalBlock = randomClusterBlock(randomVersion()); + final var projectGlobalBlock = randomClusterBlock(randomVersion()); + if (randomBoolean()) { + builder.addIndexBlock(project1, project1Index, indexBlock); + } + builder.addGlobalBlock(globalBlock); + builder.addProjectGlobalBlock(project1, projectGlobalBlock); + var clusterBlocks = builder.build(); + assertThat(clusterBlocks.global().size(), equalTo(1)); + assertThat(clusterBlocks.projectGlobal(project1).size(), equalTo(1)); + assertThat(clusterBlocks.projectGlobal(project2).size(), equalTo(0)); + assertThat(clusterBlocks.global(project1).size(), equalTo(2)); + assertThat(clusterBlocks.global(project2).size(), equalTo(1)); + assertTrue(clusterBlocks.indexBlocked(project1, randomFrom(projectGlobalBlock.levels()), project1Index)); + } + private static ClusterBlock randomClusterBlock(TransportVersion version) { final String uuid = randomBoolean() ? UUIDs.randomBase64UUID() : null; final EnumSet levels = ClusterBlock.filterLevels( diff --git a/server/src/test/java/org/elasticsearch/cluster/block/ClusterBlocksSerializationTests.java b/server/src/test/java/org/elasticsearch/cluster/block/ClusterBlocksSerializationTests.java index be9267116a490..a4a1134802f36 100644 --- a/server/src/test/java/org/elasticsearch/cluster/block/ClusterBlocksSerializationTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/block/ClusterBlocksSerializationTests.java @@ -15,6 +15,7 @@ import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.cluster.metadata.ProjectId; +import org.elasticsearch.cluster.metadata.ProjectMetadata; import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; @@ -57,6 +58,9 @@ protected ClusterBlocksTestWrapper createTestInstance() { if (randomBoolean()) { builder.addIndexBlock(randomUniqueProjectId(), randomIdentifier(), randomIndexBlock()); } + if (randomBoolean()) { + builder.addProjectGlobalBlock(randomUniqueProjectId(), ProjectMetadata.PROJECT_UNDER_DELETION_BLOCK); + } return new ClusterBlocksTestWrapper(builder.build()); } @@ -64,7 +68,7 @@ protected ClusterBlocksTestWrapper createTestInstance() { protected ClusterBlocksTestWrapper mutateInstance(ClusterBlocksTestWrapper instance) throws IOException { final ClusterBlocks clusterBlocks = instance.clusterBlocks(); final var builder = ClusterBlocks.builder(clusterBlocks); - return switch (between(0, 2)) { + return switch (between(0, 3)) { case 0 -> { final Set globalBlocks = clusterBlocks.global(); if (globalBlocks.isEmpty()) { @@ -76,7 +80,10 @@ protected ClusterBlocksTestWrapper mutateInstance(ClusterBlocksTestWrapper insta yield new ClusterBlocksTestWrapper(builder.build()); } case 1 -> { - if (clusterBlocks.noIndexBlockAllProjects()) { + boolean noProjectHasAnIndexBlock = clusterBlocks.projectBlocksMap.values() + .stream() + .allMatch(projectBlocks -> projectBlocks.indices().isEmpty()); + if (noProjectHasAnIndexBlock) { builder.addIndexBlock(randomProjectIdOrDefault(), randomIdentifier(), randomIndexBlock()); } else { if (randomBoolean()) { @@ -105,6 +112,23 @@ protected ClusterBlocksTestWrapper mutateInstance(ClusterBlocksTestWrapper insta builder.addIndexBlock(randomUniqueProjectId(), randomIdentifier(), randomIndexBlock()); yield new ClusterBlocksTestWrapper(builder.build()); } + case 3 -> { + Set projectsWithProjectGlobalBlock = clusterBlocks.projectBlocksMap.entrySet() + .stream() + .filter(e -> e.getValue().projectGlobals().isEmpty() == false) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + if (projectsWithProjectGlobalBlock.isEmpty() == false && randomBoolean()) { + builder.removeProjectGlobalBlock( + randomFrom(projectsWithProjectGlobalBlock), + ProjectMetadata.PROJECT_UNDER_DELETION_BLOCK + ); + } else { + var newProjectId = randomUniqueProjectId(); + builder.addProjectGlobalBlock(newProjectId, ProjectMetadata.PROJECT_UNDER_DELETION_BLOCK); + } + yield new ClusterBlocksTestWrapper(builder.build()); + } default -> throw new AssertionError("Illegal randomisation branch"); }; } @@ -168,6 +192,9 @@ public void testDiff() throws IOException { if (randomBoolean()) { builder.addIndexBlock(Metadata.DEFAULT_PROJECT_ID, indexName, block); } + if (randomBoolean()) { + builder.addProjectGlobalBlock(randomUniqueProjectId(), ProjectMetadata.PROJECT_UNDER_DELETION_BLOCK); + } final ClusterBlocks current = builder.build(); final var diff = current.diff(base); @@ -272,12 +299,13 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; ClusterBlocksTestWrapper that = (ClusterBlocksTestWrapper) o; return clusterBlocks.global().equals(that.clusterBlocks.global()) - && indicesBlocksAllProjects().equals(that.indicesBlocksAllProjects()); + && indicesBlocksAllProjects().equals(that.indicesBlocksAllProjects()) + && projectBlocksAllProjects().equals(that.projectBlocksAllProjects()); } @Override public int hashCode() { - return Objects.hash(clusterBlocks.global(), indicesBlocksAllProjects()); + return Objects.hash(clusterBlocks.global(), indicesBlocksAllProjects(), projectBlocksAllProjects()); } @Override @@ -295,5 +323,11 @@ private Map>> indicesBlocksAllProjects( .stream() .collect(Collectors.toUnmodifiableMap(Function.identity(), clusterBlocks::indices)); } + + private Map> projectBlocksAllProjects() { + return clusterBlocks.projectBlocksMap.keySet() + .stream() + .collect(Collectors.toUnmodifiableMap(Function.identity(), clusterBlocks::projectGlobal)); + } } }