Skip to content

Commit 1a90689

Browse files
authored
Implement framework for migrating system indices (elastic#78951)
This PR adds a framework for migrating system indices as necessary prior to Elasticsearch upgrades. This framework uses REST APIs added in another commit: - GET _migration/system_features This API, which gets the status of "features" (plugins which own system indices) with regards to whether they need to be upgraded or not. As of this PR, this API also reports errors encountered while migrating system indices alongside the index that was being processed when this occurred. As an example of this error reporting: ```json { "feature_name": "logstash_management", "minimum_index_version": "8.0.0", "upgrade_status": "ERROR", "indices": [ { "index": ".logstash", "version": "8.0.0", "failure_cause": { "error": { "root_cause": [ { "type": "runtime_exception", "reason": "whoopsie", "stack_trace": "<omitted for brevity>" } ], "type": "runtime_exception", "reason": "whoopsie", "stack_trace": "<omitted for brevity>" } } } ] } ``` - POST _migration/system_features This API starts the migration process. The API for this has no changes, but when called, any system indices which need to be migrated will be migrated, with status information stored in the cluster state for later use by the GET _migration/system_features API.
1 parent 008c55b commit 1a90689

File tree

28 files changed

+3079
-192
lines changed

28 files changed

+3079
-192
lines changed

client/rest-high-level/src/test/java/org/elasticsearch/client/MigrationIT.java

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323

2424
import static org.hamcrest.Matchers.equalTo;
2525
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
26+
import static org.hamcrest.Matchers.hasSize;
2627
import static org.hamcrest.Matchers.is;
27-
import static org.hamcrest.Matchers.nullValue;
2828

2929
public class MigrationIT extends ESRestHighLevelClientTestCase {
3030

@@ -60,11 +60,8 @@ public void testGetFeatureUpgradeStatus() throws IOException {
6060
public void testPostFeatureUpgradeStatus() throws IOException {
6161
PostFeatureUpgradeRequest request = new PostFeatureUpgradeRequest();
6262
PostFeatureUpgradeResponse response = highLevelClient().migration().postFeatureUpgrade(request, RequestOptions.DEFAULT);
63-
assertThat(response.isAccepted(), equalTo(true));
64-
assertThat(response.getFeatures().size(), equalTo(1));
65-
PostFeatureUpgradeResponse.Feature feature = response.getFeatures().get(0);
66-
assertThat(feature.getFeatureName(), equalTo("security"));
67-
assertThat(response.getReason(), nullValue());
68-
assertThat(response.getElasticsearchException(), nullValue());
63+
assertThat(response.isAccepted(), equalTo(false));
64+
assertThat(response.getFeatures(), hasSize(0));
65+
assertThat(response.getReason(), equalTo("No system indices require migration"));
6966
}
7067
}

client/rest-high-level/src/test/java/org/elasticsearch/client/migration/GetFeatureUpgradeStatusResponseTests.java

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,14 @@ protected org.elasticsearch.action.admin.cluster.migration.GetFeatureUpgradeStat
4040
randomAlphaOfLengthBetween(3, 20),
4141
randomFrom(Version.CURRENT, Version.CURRENT.minimumCompatibilityVersion()),
4242
randomFrom(org.elasticsearch.action.admin.cluster.migration.GetFeatureUpgradeStatusResponse.UpgradeStatus.values()),
43-
randomList(4,
44-
() -> new org.elasticsearch.action.admin.cluster.migration.GetFeatureUpgradeStatusResponse.IndexVersion(
43+
randomList(
44+
4,
45+
() -> new org.elasticsearch.action.admin.cluster.migration.GetFeatureUpgradeStatusResponse.IndexInfo(
4546
randomAlphaOfLengthBetween(3, 20),
46-
randomFrom(Version.CURRENT, Version.CURRENT.minimumCompatibilityVersion())))
47+
randomFrom(Version.CURRENT, Version.CURRENT.minimumCompatibilityVersion()),
48+
null
49+
)
50+
)
4751
)),
4852
randomFrom(org.elasticsearch.action.admin.cluster.migration.GetFeatureUpgradeStatusResponse.UpgradeStatus.values())
4953
);
@@ -78,12 +82,12 @@ protected void assertInstances(
7882
assertThat(clientStatus.getIndexVersions(), hasSize(serverTestStatus.getIndexVersions().size()));
7983

8084
for (int j = 0; i < clientStatus.getIndexVersions().size(); i++) {
81-
org.elasticsearch.action.admin.cluster.migration.GetFeatureUpgradeStatusResponse.IndexVersion serverIndexVersion
85+
org.elasticsearch.action.admin.cluster.migration.GetFeatureUpgradeStatusResponse.IndexInfo serverIndexInfo
8286
= serverTestStatus.getIndexVersions().get(j);
8387
GetFeatureUpgradeStatusResponse.IndexVersion clientIndexVersion = clientStatus.getIndexVersions().get(j);
8488

85-
assertThat(clientIndexVersion.getIndexName(), equalTo(serverIndexVersion.getIndexName()));
86-
assertThat(clientIndexVersion.getVersion(), equalTo(serverIndexVersion.getVersion().toString()));
89+
assertThat(clientIndexVersion.getIndexName(), equalTo(serverIndexInfo.getIndexName()));
90+
assertThat(clientIndexVersion.getVersion(), equalTo(serverIndexInfo.getVersion().toString()));
8791
}
8892
}
8993
}

docs/reference/migration/apis/feature_upgrade.asciidoc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ and to trigger an automated system upgrade that might potentially involve downti
2424
==== {api-prereq-title}
2525

2626
* If the {es} {security-features} are enabled, you must have the `manage`
27-
<<privileges-list-cluster,cluster privilege>> to use this API. (TODO: true?)
27+
<<privileges-list-cluster,cluster privilege>> to use this API.
2828

2929
[[feature-upgrade-api-example]]
3030
==== {api-examples-title}
@@ -144,6 +144,7 @@ Example response:
144144
]
145145
}
146146
--------------------------------------------------
147+
// TESTRESPONSE[skip: can't actually upgrade system indices in these tests]
147148

148149
This tells us that the security index is being upgraded. To check the
149150
overall status of the upgrade, call the endpoint with GET.

modules/reindex/src/internalClusterTest/java/org/elasticsearch/migration/FeatureMigrationIT.java

Lines changed: 388 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.migration;
10+
11+
import org.apache.lucene.util.SetOnce;
12+
import org.elasticsearch.Version;
13+
import org.elasticsearch.action.ActionListener;
14+
import org.elasticsearch.action.admin.cluster.migration.GetFeatureUpgradeStatusAction;
15+
import org.elasticsearch.action.admin.cluster.migration.GetFeatureUpgradeStatusRequest;
16+
import org.elasticsearch.action.admin.cluster.migration.GetFeatureUpgradeStatusResponse;
17+
import org.elasticsearch.action.admin.cluster.migration.PostFeatureUpgradeAction;
18+
import org.elasticsearch.action.admin.cluster.migration.PostFeatureUpgradeRequest;
19+
import org.elasticsearch.action.admin.cluster.migration.PostFeatureUpgradeResponse;
20+
import org.elasticsearch.client.Client;
21+
import org.elasticsearch.cluster.ClusterState;
22+
import org.elasticsearch.cluster.metadata.Metadata;
23+
import org.elasticsearch.cluster.service.ClusterService;
24+
import org.elasticsearch.common.Strings;
25+
import org.elasticsearch.common.settings.Settings;
26+
import org.elasticsearch.indices.SystemIndexDescriptor;
27+
import org.elasticsearch.plugins.Plugin;
28+
import org.elasticsearch.plugins.SystemIndexPlugin;
29+
import org.elasticsearch.reindex.ReindexPlugin;
30+
import org.elasticsearch.upgrades.FeatureMigrationResults;
31+
32+
import java.util.ArrayList;
33+
import java.util.Arrays;
34+
import java.util.Collection;
35+
import java.util.Collections;
36+
import java.util.HashMap;
37+
import java.util.List;
38+
import java.util.Map;
39+
import java.util.Set;
40+
import java.util.concurrent.atomic.AtomicReference;
41+
import java.util.function.BiConsumer;
42+
import java.util.function.Function;
43+
import java.util.stream.Collectors;
44+
45+
import static org.hamcrest.Matchers.aMapWithSize;
46+
import static org.hamcrest.Matchers.allOf;
47+
import static org.hamcrest.Matchers.equalTo;
48+
import static org.hamcrest.Matchers.hasEntry;
49+
import static org.hamcrest.Matchers.hasItems;
50+
import static org.hamcrest.Matchers.hasKey;
51+
import static org.hamcrest.Matchers.is;
52+
import static org.hamcrest.Matchers.notNullValue;
53+
import static org.hamcrest.Matchers.nullValue;
54+
55+
public class MultiFeatureMigrationIT extends FeatureMigrationIT {
56+
57+
@Override
58+
protected Settings nodeSettings(int nodeOrdinal, Settings otherSettings) {
59+
return Settings.builder().put(super.nodeSettings(nodeOrdinal, otherSettings)).build();
60+
}
61+
62+
@Override
63+
protected boolean forbidPrivateIndexSettings() {
64+
// We need to be able to set the index creation version manually.
65+
return false;
66+
}
67+
68+
@Override
69+
protected Collection<Class<? extends Plugin>> nodePlugins() {
70+
List<Class<? extends Plugin>> plugins = new ArrayList<>(super.nodePlugins());
71+
plugins.add(FeatureMigrationIT.TestPlugin.class);
72+
plugins.add(SecondPlugin.class);
73+
plugins.add(ReindexPlugin.class);
74+
return plugins;
75+
}
76+
77+
// Sorts alphabetically after the feature from MultiFeatureMigrationIT
78+
private static final String SECOND_FEATURE_NAME = "B-test-feature";
79+
private static final String ORIGIN = MultiFeatureMigrationIT.class.getSimpleName();
80+
private static final String VERSION_META_KEY = "version";
81+
static final int SECOND_FEATURE_IDX_FLAG_VALUE = 0;
82+
83+
public void testMultipleFeatureMigration() throws Exception {
84+
// All the indices from FeatureMigrationIT
85+
createSystemIndexForDescriptor(INTERNAL_MANAGED);
86+
createSystemIndexForDescriptor(INTERNAL_UNMANAGED);
87+
createSystemIndexForDescriptor(EXTERNAL_MANAGED);
88+
createSystemIndexForDescriptor(EXTERNAL_UNMANAGED);
89+
// And our new one
90+
createSystemIndexForDescriptor(SECOND_FEATURE_IDX_DESCIPTOR);
91+
92+
ensureGreen();
93+
94+
SetOnce<Boolean> preMigrationHookCalled = new SetOnce<>();
95+
SetOnce<Boolean> postMigrationHookCalled = new SetOnce<>();
96+
SetOnce<Boolean> secondPluginPreMigrationHookCalled = new SetOnce<>();
97+
SetOnce<Boolean> secondPluginPostMigrationHookCalled = new SetOnce<>();
98+
99+
TestPlugin.preMigrationHook.set(clusterState -> {
100+
// None of the other hooks should have been called yet.
101+
assertThat(postMigrationHookCalled.get(), nullValue());
102+
assertThat(secondPluginPreMigrationHookCalled.get(), nullValue());
103+
assertThat(secondPluginPostMigrationHookCalled.get(), nullValue());
104+
Map<String, Object> metadata = new HashMap<>();
105+
metadata.put("stringKey", "first plugin value");
106+
107+
// We shouldn't have any results in the cluster state given no features have finished yet.
108+
FeatureMigrationResults currentResults = clusterState.metadata().custom(FeatureMigrationResults.TYPE);
109+
assertThat(currentResults, nullValue());
110+
111+
preMigrationHookCalled.set(true);
112+
return metadata;
113+
});
114+
115+
TestPlugin.postMigrationHook.set((clusterState, metadata) -> {
116+
// Check that the hooks have been called or not as expected.
117+
assertThat(preMigrationHookCalled.get(), is(true));
118+
assertThat(secondPluginPreMigrationHookCalled.get(), nullValue());
119+
assertThat(secondPluginPostMigrationHookCalled.get(), nullValue());
120+
121+
assertThat(
122+
metadata,
123+
hasEntry("stringKey", "first plugin value")
124+
);
125+
126+
// We shouldn't have any results in the cluster state given no features have finished yet.
127+
FeatureMigrationResults currentResults = clusterState.metadata().custom(FeatureMigrationResults.TYPE);
128+
assertThat(currentResults, nullValue());
129+
130+
postMigrationHookCalled.set(true);
131+
});
132+
133+
SecondPlugin.preMigrationHook.set(clusterState -> {
134+
// Check that the hooks have been called or not as expected.
135+
assertThat(preMigrationHookCalled.get(), is(true));
136+
assertThat(postMigrationHookCalled.get(), is(true));
137+
assertThat(secondPluginPostMigrationHookCalled.get(), nullValue());
138+
139+
Map<String, Object> metadata = new HashMap<>();
140+
metadata.put("stringKey", "second plugin value");
141+
142+
// But now, we should have results, as we're in a new feature!
143+
FeatureMigrationResults currentResults = clusterState.metadata().custom(FeatureMigrationResults.TYPE);
144+
assertThat(currentResults, notNullValue());
145+
assertThat(currentResults.getFeatureStatuses(), allOf(aMapWithSize(1), hasKey(FEATURE_NAME)));
146+
assertThat(currentResults.getFeatureStatuses().get(FEATURE_NAME).succeeded(), is(true));
147+
assertThat(currentResults.getFeatureStatuses().get(FEATURE_NAME).getFailedIndexName(), nullValue());
148+
assertThat(currentResults.getFeatureStatuses().get(FEATURE_NAME).getException(), nullValue());
149+
150+
secondPluginPreMigrationHookCalled.set(true);
151+
return metadata;
152+
});
153+
154+
SecondPlugin.postMigrationHook.set((clusterState, metadata) -> {
155+
// Check that the hooks have been called or not as expected.
156+
assertThat(preMigrationHookCalled.get(), is(true));
157+
assertThat(postMigrationHookCalled.get(), is(true));
158+
assertThat(secondPluginPreMigrationHookCalled.get(), is(true));
159+
160+
assertThat(
161+
metadata,
162+
hasEntry("stringKey", "second plugin value")
163+
);
164+
165+
// And here, the results should be the same, as we haven't updated the state with this feature's status yet.
166+
FeatureMigrationResults currentResults = clusterState.metadata().custom(FeatureMigrationResults.TYPE);
167+
assertThat(currentResults, notNullValue());
168+
assertThat(currentResults.getFeatureStatuses(), allOf(aMapWithSize(1), hasKey(FEATURE_NAME)));
169+
assertThat(currentResults.getFeatureStatuses().get(FEATURE_NAME).succeeded(), is(true));
170+
assertThat(currentResults.getFeatureStatuses().get(FEATURE_NAME).getFailedIndexName(), nullValue());
171+
assertThat(currentResults.getFeatureStatuses().get(FEATURE_NAME).getException(), nullValue());
172+
173+
secondPluginPostMigrationHookCalled.set(true);
174+
});
175+
176+
PostFeatureUpgradeRequest migrationRequest = new PostFeatureUpgradeRequest();
177+
PostFeatureUpgradeResponse migrationResponse = client().execute(PostFeatureUpgradeAction.INSTANCE, migrationRequest).get();
178+
assertThat(migrationResponse.getReason(), nullValue());
179+
assertThat(migrationResponse.getElasticsearchException(), nullValue());
180+
final Set<String> migratingFeatures = migrationResponse.getFeatures()
181+
.stream()
182+
.map(PostFeatureUpgradeResponse.Feature::getFeatureName)
183+
.collect(Collectors.toSet());
184+
assertThat(migratingFeatures, hasItems(FEATURE_NAME, SECOND_FEATURE_NAME));
185+
186+
GetFeatureUpgradeStatusRequest getStatusRequest = new GetFeatureUpgradeStatusRequest();
187+
assertBusy(() -> {
188+
GetFeatureUpgradeStatusResponse statusResponse = client().execute(GetFeatureUpgradeStatusAction.INSTANCE, getStatusRequest)
189+
.get();
190+
logger.info(Strings.toString(statusResponse));
191+
assertThat(statusResponse.getUpgradeStatus(), equalTo(GetFeatureUpgradeStatusResponse.UpgradeStatus.NO_MIGRATION_NEEDED));
192+
});
193+
194+
assertTrue("the first plugin's pre-migration hook wasn't actually called", preMigrationHookCalled.get());
195+
assertTrue("the first plugin's post-migration hook wasn't actually called", postMigrationHookCalled.get());
196+
197+
assertTrue("the second plugin's pre-migration hook wasn't actually called", secondPluginPreMigrationHookCalled.get());
198+
assertTrue("the second plugin's post-migration hook wasn't actually called", secondPluginPostMigrationHookCalled.get());
199+
200+
Metadata finalMetadata = client().admin().cluster().prepareState().get().getState().metadata();
201+
// Check that the results metadata is what we expect
202+
FeatureMigrationResults currentResults = finalMetadata.custom(FeatureMigrationResults.TYPE);
203+
assertThat(currentResults, notNullValue());
204+
assertThat(currentResults.getFeatureStatuses(), allOf(aMapWithSize(2), hasKey(FEATURE_NAME), hasKey(SECOND_FEATURE_NAME)));
205+
assertThat(currentResults.getFeatureStatuses().get(FEATURE_NAME).succeeded(), is(true));
206+
assertThat(currentResults.getFeatureStatuses().get(FEATURE_NAME).getFailedIndexName(), nullValue());
207+
assertThat(currentResults.getFeatureStatuses().get(FEATURE_NAME).getException(), nullValue());
208+
assertThat(currentResults.getFeatureStatuses().get(SECOND_FEATURE_NAME).succeeded(), is(true));
209+
assertThat(currentResults.getFeatureStatuses().get(SECOND_FEATURE_NAME).getFailedIndexName(), nullValue());
210+
assertThat(currentResults.getFeatureStatuses().get(SECOND_FEATURE_NAME).getException(), nullValue());
211+
212+
// Finally, verify that all the indices exist and have the properties we expect.
213+
assertIndexHasCorrectProperties(
214+
finalMetadata,
215+
".int-man-old-reindexed-for-8",
216+
INTERNAL_MANAGED_FLAG_VALUE,
217+
true,
218+
true,
219+
Arrays.asList(".int-man-old", ".internal-managed-alias")
220+
);
221+
assertIndexHasCorrectProperties(
222+
finalMetadata,
223+
".int-unman-old-reindexed-for-8",
224+
INTERNAL_UNMANAGED_FLAG_VALUE,
225+
false,
226+
true,
227+
Collections.singletonList(".int-unman-old")
228+
);
229+
assertIndexHasCorrectProperties(
230+
finalMetadata,
231+
".ext-man-old-reindexed-for-8",
232+
EXTERNAL_MANAGED_FLAG_VALUE,
233+
true,
234+
false,
235+
Arrays.asList(".ext-man-old", ".external-managed-alias")
236+
);
237+
assertIndexHasCorrectProperties(
238+
finalMetadata,
239+
".ext-unman-old-reindexed-for-8",
240+
EXTERNAL_UNMANAGED_FLAG_VALUE,
241+
false,
242+
false,
243+
Collections.singletonList(".ext-unman-old")
244+
);
245+
246+
assertIndexHasCorrectProperties(
247+
finalMetadata,
248+
".second-int-man-old-reindexed-for-8",
249+
SECOND_FEATURE_IDX_FLAG_VALUE,
250+
true,
251+
true,
252+
Arrays.asList(".second-int-man-old", ".second-internal-managed-alias")
253+
);
254+
}
255+
256+
private static final SystemIndexDescriptor SECOND_FEATURE_IDX_DESCIPTOR = SystemIndexDescriptor.builder()
257+
.setIndexPattern(".second-int-man-*")
258+
.setAliasName(".second-internal-managed-alias")
259+
.setPrimaryIndex(".second-int-man-old")
260+
.setType(SystemIndexDescriptor.Type.INTERNAL_MANAGED)
261+
.setSettings(createSimpleSettings(Version.V_7_0_0, 0))
262+
.setMappings(createSimpleMapping(true, true))
263+
.setOrigin(ORIGIN)
264+
.setVersionMetaKey(VERSION_META_KEY)
265+
.setAllowedElasticProductOrigins(Collections.emptyList())
266+
.setMinimumNodeVersion(Version.V_7_0_0)
267+
.setPriorSystemIndexDescriptors(Collections.emptyList())
268+
.build();
269+
270+
public static class SecondPlugin extends Plugin implements SystemIndexPlugin {
271+
272+
private static final AtomicReference<Function<ClusterState, Map<String, Object>>> preMigrationHook = new AtomicReference<>();
273+
private static final AtomicReference<BiConsumer<ClusterState, Map<String, Object>>> postMigrationHook = new AtomicReference<>();
274+
275+
public SecondPlugin() {
276+
277+
}
278+
279+
@Override public String getFeatureName() {
280+
return SECOND_FEATURE_NAME;
281+
}
282+
283+
@Override public String getFeatureDescription() {
284+
return "a plugin for test system index migration with multiple features";
285+
}
286+
287+
@Override public Collection<SystemIndexDescriptor> getSystemIndexDescriptors(Settings settings) {
288+
return Collections.singletonList(SECOND_FEATURE_IDX_DESCIPTOR);
289+
}
290+
291+
@Override public void prepareForIndicesMigration(
292+
ClusterService clusterService, Client client, ActionListener<Map<String, Object>> listener) {
293+
listener.onResponse(preMigrationHook.get().apply(clusterService.state()));
294+
}
295+
296+
@Override public void indicesMigrationComplete(
297+
Map<String, Object> preUpgradeMetadata, ClusterService clusterService, Client client, ActionListener<Boolean> listener) {
298+
postMigrationHook.get().accept(clusterService.state(), preUpgradeMetadata);
299+
listener.onResponse(true);
300+
}
301+
}
302+
}

qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/FeatureUpgradeIT.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ public void testGetFeatureUpgradeStatus() throws Exception {
9191

9292
assertThat(feature.size(), equalTo(4));
9393
assertThat(feature.get("minimum_index_version"), equalTo(UPGRADE_FROM_VERSION.toString()));
94-
if (UPGRADE_FROM_VERSION.before(Version.CURRENT.minimumIndexCompatibilityVersion())) {
94+
if (UPGRADE_FROM_VERSION.before(Version.V_8_0_0)) {
9595
assertThat(feature.get("migration_status"), equalTo("MIGRATION_NEEDED"));
9696
} else {
9797
assertThat(feature.get("migration_status"), equalTo("NO_MIGRATION_NEEDED"));

0 commit comments

Comments
 (0)