Skip to content

Commit a1c2020

Browse files
authored
More reliable trigger for security index migration (#139028) (#139038)
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 b76aba2 commit a1c2020

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
@@ -784,7 +784,7 @@ public void clusterChanged(ClusterChangedEvent event) {
784784
final IndexState previousState = getProjectState(projectId, previousStateByProject);
785785
final IndexState newState = updateProjectState(clusterState.projectState(projectId));
786786

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

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

106+
/**
107+
* @return The highest migration version that is currently defined
108+
*/
109+
public static int highestMigrationVersion() {
110+
return MIGRATIONS_BY_VERSION.lastKey();
111+
}
112+
106113
public static class RoleMetadataFlattenedMigration implements SecurityMigration {
107114

108115
@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)