Skip to content

Add projectsDelta method to ClusterChangedEvent #127697

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.elasticsearch.cluster.metadata.IndexGraveyard.IndexGraveyardDiff;
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.cluster.node.DiscoveryNodes;
import org.elasticsearch.cluster.routing.IndexRoutingTable;
Expand Down Expand Up @@ -41,6 +42,8 @@ public class ClusterChangedEvent {

private final DiscoveryNodes.Delta nodesDelta;

private final ProjectsDelta projectsDelta;

public ClusterChangedEvent(String source, ClusterState state, ClusterState previousState) {
Objects.requireNonNull(source, "source must not be null");
Objects.requireNonNull(state, "state must not be null");
Expand All @@ -49,6 +52,7 @@ public ClusterChangedEvent(String source, ClusterState state, ClusterState previ
this.state = state;
this.previousState = previousState;
this.nodesDelta = state.nodes().delta(previousState.nodes());
this.projectsDelta = calculateProjectDelta(previousState.metadata(), state.metadata());
}

/**
Expand Down Expand Up @@ -237,6 +241,13 @@ public boolean nodesChanged() {
return nodesRemoved() || nodesAdded();
}

/**
* Returns the {@link ProjectsDelta} between the previous cluster state and the new cluster state.
*/
public ProjectsDelta projectDelta() {
return projectsDelta;
}

/**
* Determines whether or not the current cluster state represents an entirely
* new cluster, either when a node joins a cluster for the first time or when
Expand Down Expand Up @@ -336,4 +347,32 @@ private List<Index> indicesDeletedFromTombstones() {
.toList();
}

private static ProjectsDelta calculateProjectDelta(Metadata previousMetadata, Metadata currentMetadata) {
if (previousMetadata.projects().size() == 1
&& previousMetadata.hasProject(ProjectId.DEFAULT)
&& currentMetadata.projects().size() == 1
&& currentMetadata.hasProject(ProjectId.DEFAULT)) {
return ProjectsDelta.EMPTY;
}

final Set<ProjectId> added = new HashSet<>();
final Set<ProjectId> removed = new HashSet<>(previousMetadata.projects().keySet());
for (var currentProject : currentMetadata.projects().keySet()) {
if (removed.remove(currentProject) == false) {
added.add(currentProject);
}
}
assert added.contains(ProjectId.DEFAULT) == false;
assert removed.contains(ProjectId.DEFAULT) == false;

return new ProjectsDelta(Collections.unmodifiableSet(added), Collections.unmodifiableSet(removed));
}

public record ProjectsDelta(Set<ProjectId> added, Set<ProjectId> removed) {
private static final ProjectsDelta EMPTY = new ProjectsDelta(Set.of(), Set.of());

public boolean isEmpty() {
return added.isEmpty() && removed.isEmpty();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
import org.elasticsearch.cluster.metadata.IndexGraveyard;
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.cluster.metadata.ReservedStateMetadata;
import org.elasticsearch.cluster.node.DiscoveryNode;
import org.elasticsearch.cluster.node.DiscoveryNodeRole;
import org.elasticsearch.cluster.node.DiscoveryNodeUtils;
Expand Down Expand Up @@ -520,6 +522,65 @@ public void testChangedCustomMetadataSetMultiProject() {
assertEquals(Set.of(IndexGraveyard.TYPE, project2Custom.getWriteableName()), event.changedCustomProjectMetadataSet());
}

public void testProjectsDelta() {
final var state0 = ClusterState.builder(TEST_CLUSTER_NAME).build();

// No project changes
final var state1 = ClusterState.builder(state0)
.metadata(Metadata.builder(state0.metadata()).put(ReservedStateMetadata.builder("test").build()))
.build();
ClusterChangedEvent event = new ClusterChangedEvent("test", state1, state0);
assertTrue(event.projectDelta().isEmpty());

// Add projects
final List<ProjectId> projectIds = randomList(1, 5, ESTestCase::randomUniqueProjectId);
Metadata.Builder metadataBuilder = Metadata.builder(state1.metadata());
for (ProjectId projectId : projectIds) {
metadataBuilder.put(ProjectMetadata.builder(projectId));
}
final var state2 = ClusterState.builder(state1).metadata(metadataBuilder.build()).build();
event = new ClusterChangedEvent("test", state2, state1);
assertThat(event.projectDelta().added(), containsInAnyOrder(projectIds.toArray()));
assertThat(event.projectDelta().removed(), empty());

// Add more projects and delete one
final var removedProjectIds = randomNonEmptySubsetOf(projectIds);
final List<ProjectId> moreProjectIds = randomList(1, 3, ESTestCase::randomUniqueProjectId);
metadataBuilder = Metadata.builder(state2.metadata());
GlobalRoutingTable.Builder routingTableBuilder = GlobalRoutingTable.builder(state2.globalRoutingTable());
for (ProjectId projectId : removedProjectIds) {
metadataBuilder.removeProject(projectId);
routingTableBuilder.removeProject(projectId);
}
for (ProjectId projectId : moreProjectIds) {
metadataBuilder.put(ProjectMetadata.builder(projectId));
}

final var state3 = ClusterState.builder(state2).metadata(metadataBuilder.build()).routingTable(routingTableBuilder.build()).build();

event = new ClusterChangedEvent("test", state3, state2);
assertThat(event.projectDelta().added(), containsInAnyOrder(moreProjectIds.toArray()));
assertThat(event.projectDelta().removed(), containsInAnyOrder(removedProjectIds.toArray()));

// Remove all projects
final List<ProjectId> remainingProjects = state3.metadata()
.projects()
.keySet()
.stream()
.filter(projectId -> ProjectId.DEFAULT.equals(projectId) == false)
.toList();
metadataBuilder = Metadata.builder(state3.metadata());
routingTableBuilder = GlobalRoutingTable.builder(state3.globalRoutingTable());
for (ProjectId projectId : remainingProjects) {
metadataBuilder.removeProject(projectId);
routingTableBuilder.removeProject(projectId);
}
final var state4 = ClusterState.builder(state3).metadata(metadataBuilder.build()).routingTable(routingTableBuilder.build()).build();
event = new ClusterChangedEvent("test", state4, state3);
assertThat(event.projectDelta().added(), empty());
assertThat(event.projectDelta().removed(), containsInAnyOrder(remainingProjects.toArray()));
}

private static class CustomClusterMetadata2 extends TestClusterCustomMetadata {
protected CustomClusterMetadata2(String data) {
super(data);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public void testStartStopDuringClusterChanges() {
final String localNodeId = randomIdentifier();
final String alternateNodeId = randomIdentifier();

final Metadata.Builder metadataBuilder = Metadata.builder();
final Metadata.Builder metadataBuilder = Metadata.builder(Metadata.EMPTY_METADATA);
final GlobalRoutingTable.Builder grtBuilder = GlobalRoutingTable.builder();

final ProjectId p1 = ProjectId.fromId("p1");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,8 +330,8 @@ public void testSecurityIndexDeleted() {

synchronizer.clusterChanged(event(currentClusterState, previousClusterState));

verify(previousClusterState, times(1)).metadata();
verify(currentClusterState, times(1)).metadata();
verify(previousClusterState, times(2)).metadata();
verify(currentClusterState, times(2)).metadata();
verifyNoMoreInteractions(nativeRolesStore, featureService, taskQueue, reservedRolesProvider, threadPool, clusterService);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.oneOf;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
Expand Down Expand Up @@ -315,10 +316,13 @@ public void testIndexHealthChangeListeners() {
final AtomicReference<IndexState> previousState = new AtomicReference<>();
final AtomicReference<IndexState> currentState = new AtomicReference<>();
final TriConsumer<ProjectId, IndexState, IndexState> listener = (projId, prevState, state) -> {
projectIdRef.set(projId);
previousState.set(prevState);
currentState.set(state);
listenerCalled.set(true);
assertThat(projId, oneOf(ProjectId.DEFAULT, projectId));
if (projId.equals(projectId)) {
projectIdRef.set(projId);
previousState.set(prevState);
currentState.set(state);
listenerCalled.set(true);
}
};
manager.addStateListener(listener);

Expand Down Expand Up @@ -573,9 +577,11 @@ public void testIndexOutOfDateListeners() {
manager.clusterChanged(new ClusterChangedEvent("test-event", clusterState, clusterState));
AtomicBoolean upToDateChanged = new AtomicBoolean();
manager.addStateListener((projId, prev, current) -> {
assertThat(projId, equalTo(projectId));
listenerCalled.set(true);
upToDateChanged.set(prev.isIndexUpToDate != current.isIndexUpToDate);
assertThat(projId, oneOf(ProjectId.DEFAULT, projectId));
if (projId.equals(projectId)) {
listenerCalled.set(true);
upToDateChanged.set(prev.isIndexUpToDate != current.isIndexUpToDate);
}
});
assertThat(manager.getProject(projectId).isIndexUpToDate(), is(true));

Expand Down Expand Up @@ -779,7 +785,7 @@ public void testGetRoleMappingsCleanupMigrationStatus() {
metadataBuilder.put(builder.build());

// No role mappings in cluster state yet
metadataBuilder.putCustom(RoleMappingMetadata.TYPE, new RoleMappingMetadata(Set.of()));
metadataBuilder.getProject(projectId).putCustom(RoleMappingMetadata.TYPE, new RoleMappingMetadata(Set.of()));

assertThat(
SecurityIndexManager.getRoleMappingsCleanupMigrationStatus(
Expand All @@ -803,10 +809,13 @@ public void testGetRoleMappingsCleanupMigrationStatus() {
metadataBuilder.put(builder.build());

// Role mappings in cluster state with fallback name
metadataBuilder.putCustom(
RoleMappingMetadata.TYPE,
new RoleMappingMetadata(Set.of(new ExpressionRoleMapping(RoleMappingMetadata.FALLBACK_NAME, null, null, null, null, true)))
);
metadataBuilder.getProject(projectId)
.putCustom(
RoleMappingMetadata.TYPE,
new RoleMappingMetadata(
Set.of(new ExpressionRoleMapping(RoleMappingMetadata.FALLBACK_NAME, null, null, null, null, true))
)
);

assertThat(
SecurityIndexManager.getRoleMappingsCleanupMigrationStatus(
Expand All @@ -830,10 +839,11 @@ public void testGetRoleMappingsCleanupMigrationStatus() {
metadataBuilder.put(builder.build());

// Role mappings in cluster state
metadataBuilder.putCustom(
RoleMappingMetadata.TYPE,
new RoleMappingMetadata(Set.of(new ExpressionRoleMapping("role_mapping_1", null, null, null, null, true)))
);
metadataBuilder.getProject(projectId)
.putCustom(
RoleMappingMetadata.TYPE,
new RoleMappingMetadata(Set.of(new ExpressionRoleMapping("role_mapping_1", null, null, null, null, true)))
);

assertThat(
SecurityIndexManager.getRoleMappingsCleanupMigrationStatus(
Expand Down Expand Up @@ -1090,7 +1100,7 @@ private ClusterState.Builder createClusterState(
String mappings,
Map<String, SystemIndexDescriptor.MappingsVersion> compatibilityVersions
) {
final Metadata metadata = Metadata.builder()
final Metadata metadata = Metadata.builder(Metadata.EMPTY_METADATA)
.put(createProjectMetadata(projectId, indexName, aliasName, format, state, mappings))
.build();
final GlobalRoutingTable routingTable = GlobalRoutingTableTestHelper.buildRoutingTable(metadata, RoutingTable.Builder::addAsNew);
Expand Down Expand Up @@ -1141,7 +1151,10 @@ public static ClusterState state(ProjectId projectId) {
.masterNodeId("1")
.localNodeId("1")
.build();
final Metadata metadata = Metadata.builder().put(ProjectMetadata.builder(projectId)).generateClusterUuidIfNeeded().build();
final Metadata metadata = Metadata.builder(Metadata.EMPTY_METADATA)
.put(ProjectMetadata.builder(projectId))
.generateClusterUuidIfNeeded()
.build();
return ClusterState.builder(CLUSTER_NAME).nodes(nodes).metadata(metadata).build();
}

Expand Down