Skip to content

Commit c54d70b

Browse files
authored
More reliable trigger for security index migration (#139028) (#139039)
We always want to trigger an index migration if one is required. However, we previously would only do that if we detected a change to the state of the security index on the master node. But a rolling upgrade might not cause a detectable change in index state - for example, in a cluster with dedicated masters nodes, if those nodes were upgraded last, then all index relocation would happen before the masters knew about the new migration (so they couldn't trigger it) and once the masters were upgraded they would detect that nothing had changed so never send a "security index changed" event and never trigger the migration task. Now, we say that if a security index exists, and it requires migration then it also triggers a change event
1 parent c0d3a73 commit c54d70b

File tree

4 files changed

+104
-2
lines changed

4 files changed

+104
-2
lines changed

docs/changelog/139028.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 139028
2+
summary: More reliable trigger for security index migration
3+
area: Security
4+
type: bug
5+
issues: []

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/SecurityIndexManager.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -785,7 +785,7 @@ public void clusterChanged(ClusterChangedEvent event) {
785785
final IndexState previousState = getProjectState(projectId, previousStateByProject);
786786
final IndexState newState = updateProjectState(clusterState.projectState(projectId));
787787

788-
if (newState.equals(previousState) == false) {
788+
if (shouldNotifyListeners(newState, previousState)) {
789789
notifications.add(() -> {
790790
for (var listener : stateChangeListeners) {
791791
listener.apply(projectId, previousState, newState);
@@ -817,6 +817,25 @@ public void clusterChanged(ClusterChangedEvent event) {
817817
notifications.forEach(Runnable::run);
818818
}
819819

820+
private static boolean shouldNotifyListeners(IndexState newState, IndexState previousState) {
821+
if (newState.equals(previousState)) {
822+
// If we add a new migration (in code), but don't change anything internal to the index state then the `IndexState`` will never
823+
// change (unless the index health changes) but we do want to treat changes to "is-up-to-date-with-migrations" as a state change
824+
// However we can't do that (easily) with a flag in `IndexState` because that flag wouldn't change - as soon as the new version
825+
// of the code was deployed it would think that the state was "not-up-to-date" and wouldn't detect a change.
826+
// Instead we just handle it as a special case here.
827+
// But, this class manages multiple different indices, not all of which have migrations defined. So we only trigger this is
828+
// the index has had at least 1 migration before (if the index is entirely new then it will be picked up by other state changes)
829+
return newState.indexExists()
830+
&& newState.securityMigrationRunning == false
831+
&& newState.migrationsVersion != null
832+
&& newState.migrationsVersion > 0
833+
&& newState.migrationsVersion < SecurityMigrations.highestMigrationVersion();
834+
} else {
835+
return true;
836+
}
837+
}
838+
820839
private IndexState updateProjectState(ProjectState project) {
821840
final IndexMetadata indexMetadata = resolveConcreteIndex(systemIndexDescriptor.getAliasName(), project.metadata());
822841
final boolean createdOnLatestVersion = isCreatedOnLatestVersion(indexMetadata);

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/SecurityMigrations.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,13 @@ default boolean checkPreConditions(SecurityIndexManager.IndexState securityIndex
115115
)
116116
);
117117

118+
/**
119+
* @return The highest migration version that is currently defined
120+
*/
121+
public static int highestMigrationVersion() {
122+
return MIGRATIONS_BY_VERSION.lastKey();
123+
}
124+
118125
public static class RoleMetadataFlattenedMigration implements SecurityMigration {
119126

120127
@Override

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/SecurityIndexManagerTests.java

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@
8585
import static org.elasticsearch.cluster.metadata.Metadata.DEFAULT_PROJECT_ID;
8686
import static org.elasticsearch.cluster.routing.GlobalRoutingTableTestHelper.routingTable;
8787
import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder;
88+
import static org.elasticsearch.xpack.core.security.action.UpdateIndexMigrationVersionAction.MIGRATION_VERSION_CUSTOM_DATA_KEY;
89+
import static org.elasticsearch.xpack.core.security.action.UpdateIndexMigrationVersionAction.MIGRATION_VERSION_CUSTOM_KEY;
8890
import static org.elasticsearch.xpack.security.support.SecurityIndexManager.FILE_SETTINGS_METADATA_NAMESPACE;
8991
import static org.hamcrest.Matchers.aMapWithSize;
9092
import static org.hamcrest.Matchers.contains;
@@ -309,6 +311,60 @@ private ClusterChangedEvent event(ClusterState clusterState) {
309311
return new ClusterChangedEvent("test-event", clusterState, EMPTY_CLUSTER_STATE);
310312
}
311313

314+
public void testStateChangeListenerIsCalledIfMigrationIsRequired() {
315+
final AtomicBoolean listenerCalled = new AtomicBoolean(false);
316+
final AtomicReference<ProjectId> projectIdRef = new AtomicReference<>();
317+
final AtomicReference<IndexState> previousState = new AtomicReference<>();
318+
final AtomicReference<IndexState> currentState = new AtomicReference<>();
319+
final TriConsumer<ProjectId, IndexState, IndexState> listener = (projId, prevState, state) -> {
320+
projectIdRef.set(projId);
321+
previousState.set(prevState);
322+
currentState.set(state);
323+
listenerCalled.set(true);
324+
};
325+
manager.addStateListener(listener);
326+
327+
// index doesn't exist and now exists
328+
ClusterState.Builder clusterStateBuilder = createClusterState(
329+
TestRestrictedIndices.INTERNAL_SECURITY_MAIN_INDEX_7,
330+
SecuritySystemIndices.SECURITY_MAIN_ALIAS
331+
);
332+
clusterStateBuilder = setMigrationVersion(
333+
clusterStateBuilder.build(),
334+
TestRestrictedIndices.INTERNAL_SECURITY_MAIN_INDEX_7,
335+
randomIntBetween(1, SecurityMigrations.highestMigrationVersion() - 1)
336+
);
337+
338+
final ClusterState clusterState = markShardsAvailable(clusterStateBuilder);
339+
manager.clusterChanged(event(clusterState));
340+
341+
assertThat(listenerCalled.get(), is(true));
342+
343+
for (int i = 0; i < 3; i++) {
344+
listenerCalled.set(false);
345+
ClusterChangedEvent event = new ClusterChangedEvent("same index state", clusterState, clusterState);
346+
manager.clusterChanged(event);
347+
348+
assertThat(listenerCalled.get(), is(true));
349+
assertThat(previousState.get(), equalTo(currentState.get()));
350+
}
351+
352+
final var newClusterState = setMigrationVersion(
353+
clusterState,
354+
TestRestrictedIndices.INTERNAL_SECURITY_MAIN_INDEX_7,
355+
SecurityMigrations.highestMigrationVersion()
356+
).build();
357+
358+
listenerCalled.set(false);
359+
manager.clusterChanged(new ClusterChangedEvent("modified index state", newClusterState, clusterState));
360+
assertThat(listenerCalled.get(), is(true));
361+
assertThat(previousState.get(), not(equalTo(currentState.get())));
362+
363+
listenerCalled.set(false);
364+
manager.clusterChanged(new ClusterChangedEvent("same index state", newClusterState, newClusterState));
365+
assertThat(listenerCalled.get(), is(false));
366+
}
367+
312368
public void testIndexHealthChangeListeners() {
313369
final AtomicBoolean listenerCalled = new AtomicBoolean(false);
314370
final AtomicReference<ProjectId> projectIdRef = new AtomicReference<>();
@@ -1123,6 +1179,22 @@ private static ProjectMetadata.Builder createProjectMetadata(ProjectId projectId
11231179
);
11241180
}
11251181

1182+
private ClusterState.Builder setMigrationVersion(ClusterState clusterState, String indexName, Integer version) {
1183+
final ClusterState.Builder csBuilder = ClusterState.builder(clusterState);
1184+
clusterState.forEachProject(project -> {
1185+
final ProjectMetadata.Builder projectBuilder = ProjectMetadata.builder(project.metadata());
1186+
final IndexMetadata.Builder indexBuilder = IndexMetadata.builder(project.metadata().index(indexName));
1187+
if (version == null) {
1188+
indexBuilder.removeCustom(MIGRATION_VERSION_CUSTOM_KEY);
1189+
} else {
1190+
indexBuilder.putCustom(MIGRATION_VERSION_CUSTOM_KEY, Map.of(MIGRATION_VERSION_CUSTOM_DATA_KEY, Integer.toString(version)));
1191+
}
1192+
projectBuilder.put(indexBuilder);
1193+
csBuilder.putProjectMetadata(projectBuilder);
1194+
});
1195+
return csBuilder;
1196+
}
1197+
11261198
private ClusterState markShardsAvailable(ClusterState.Builder clusterStateBuilder) {
11271199
final ClusterState cs = clusterStateBuilder.build();
11281200
final RoutingTable projectRouting = SecurityTestUtils.buildIndexRoutingTable(
@@ -1159,7 +1231,6 @@ private static IndexMetadata.Builder getIndexMetadata(
11591231
if (mappings != null) {
11601232
indexMetadata.putMapping(mappings);
11611233
}
1162-
11631234
return indexMetadata;
11641235
}
11651236

0 commit comments

Comments
 (0)