-
Notifications
You must be signed in to change notification settings - Fork 25k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
The new subcommand elasticsearch-node remove-index-settings can be used to remove index settings from the cluster state in case where it contains incompatible index settings that prevent the cluster from forming. This tool can cause data loss and its use should be your last resort. Relates #96075
- Loading branch information
Showing
5 changed files
with
326 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
pr: 109276 | ||
summary: Add remove index setting command | ||
area: Infra/Settings | ||
type: enhancement | ||
issues: [] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
165 changes: 165 additions & 0 deletions
165
...ClusterTest/java/org/elasticsearch/cluster/coordination/RemoveIndexSettingsCommandIT.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
package org.elasticsearch.cluster.coordination; | ||
|
||
import joptsimple.OptionSet; | ||
|
||
import org.elasticsearch.ElasticsearchException; | ||
import org.elasticsearch.cli.MockTerminal; | ||
import org.elasticsearch.cli.UserException; | ||
import org.elasticsearch.common.collect.ImmutableOpenMap; | ||
import org.elasticsearch.common.settings.Setting; | ||
import org.elasticsearch.common.settings.Settings; | ||
import org.elasticsearch.common.util.CollectionUtils; | ||
import org.elasticsearch.env.Environment; | ||
import org.elasticsearch.env.TestEnvironment; | ||
import org.elasticsearch.plugins.Plugin; | ||
import org.elasticsearch.test.ESIntegTestCase; | ||
|
||
import java.util.Arrays; | ||
import java.util.Collection; | ||
import java.util.List; | ||
|
||
import static org.hamcrest.Matchers.containsString; | ||
import static org.hamcrest.Matchers.equalTo; | ||
import static org.hamcrest.Matchers.not; | ||
|
||
@ESIntegTestCase.ClusterScope(scope = ESIntegTestCase.Scope.TEST, numDataNodes = 0, autoManageMasterNodes = false) | ||
public class RemoveIndexSettingsCommandIT extends ESIntegTestCase { | ||
|
||
static final Setting<Integer> FOO = Setting.intSetting("index.foo", 1, Setting.Property.IndexScope, Setting.Property.Dynamic); | ||
static final Setting<Integer> BAR = Setting.intSetting("index.bar", 2, Setting.Property.IndexScope, Setting.Property.Final); | ||
|
||
public static class ExtraSettingsPlugin extends Plugin { | ||
@Override | ||
public List<Setting<?>> getSettings() { | ||
return Arrays.asList(FOO, BAR); | ||
} | ||
} | ||
|
||
@Override | ||
protected Collection<Class<? extends Plugin>> nodePlugins() { | ||
return CollectionUtils.appendToCopy(super.nodePlugins(), ExtraSettingsPlugin.class); | ||
} | ||
|
||
public void testRemoveSettingsAbortedByUser() throws Exception { | ||
internalCluster().setBootstrapMasterNodeIndex(0); | ||
String node = internalCluster().startNode(); | ||
createIndex("test-index", Settings.builder().put(FOO.getKey(), 101).put(BAR.getKey(), 102).build()); | ||
ensureYellow("test-index"); | ||
Settings dataPathSettings = internalCluster().dataPathSettings(node); | ||
ensureStableCluster(1); | ||
internalCluster().stopRandomDataNode(); | ||
|
||
Settings nodeSettings = Settings.builder().put(internalCluster().getDefaultSettings()).put(dataPathSettings).build(); | ||
ElasticsearchException error = expectThrows( | ||
ElasticsearchException.class, | ||
() -> removeIndexSettings(TestEnvironment.newEnvironment(nodeSettings), true, "index.foo") | ||
); | ||
assertThat(error.getMessage(), equalTo(ElasticsearchNodeCommand.ABORTED_BY_USER_MSG)); | ||
internalCluster().startNode(nodeSettings); | ||
} | ||
|
||
public void testRemoveSettingsSuccessful() throws Exception { | ||
internalCluster().setBootstrapMasterNodeIndex(0); | ||
String node = internalCluster().startNode(); | ||
Settings dataPathSettings = internalCluster().dataPathSettings(node); | ||
|
||
int numIndices = randomIntBetween(1, 10); | ||
int[] barValues = new int[numIndices]; | ||
for (int i = 0; i < numIndices; i++) { | ||
String index = "test-index-" + i; | ||
barValues[i] = between(1, 1000); | ||
createIndex(index, Settings.builder().put(FOO.getKey(), between(1, 1000)).put(BAR.getKey(), barValues[i]).build()); | ||
} | ||
int moreIndices = randomIntBetween(1, 10); | ||
for (int i = 0; i < moreIndices; i++) { | ||
createIndex("more-index-" + i, Settings.EMPTY); | ||
} | ||
internalCluster().stopNode(node); | ||
|
||
Environment environment = TestEnvironment.newEnvironment( | ||
Settings.builder().put(internalCluster().getDefaultSettings()).put(dataPathSettings).build() | ||
); | ||
|
||
MockTerminal terminal = removeIndexSettings(environment, false, "index.foo"); | ||
assertThat(terminal.getOutput(), containsString(RemoveIndexSettingsCommand.SETTINGS_REMOVED_MSG)); | ||
for (int i = 0; i < numIndices; i++) { | ||
assertThat(terminal.getOutput(), containsString("Index setting [index.foo] will be removed from index [[test-index-" + i)); | ||
} | ||
for (int i = 0; i < moreIndices; i++) { | ||
assertThat(terminal.getOutput(), not(containsString("Index setting [index.foo] will be removed from index [[more-index-" + i))); | ||
} | ||
Settings nodeSettings = Settings.builder().put(internalCluster().getDefaultSettings()).put(dataPathSettings).build(); | ||
internalCluster().startNode(nodeSettings); | ||
|
||
ImmutableOpenMap<String, Settings> getIndexSettings = client().admin() | ||
.indices() | ||
.prepareGetSettings("test-index-*") | ||
.get() | ||
.getIndexToSettings(); | ||
for (int i = 0; i < numIndices; i++) { | ||
String index = "test-index-" + i; | ||
Settings indexSettings = getIndexSettings.get(index); | ||
assertFalse(indexSettings.hasValue("index.foo")); | ||
assertThat(indexSettings.get("index.bar"), equalTo(Integer.toString(barValues[i]))); | ||
} | ||
getIndexSettings = client().admin().indices().prepareGetSettings("more-index-*").get().getIndexToSettings(); | ||
for (int i = 0; i < moreIndices; i++) { | ||
assertNotNull(getIndexSettings.get("more-index-" + i)); | ||
} | ||
} | ||
|
||
public void testSettingDoesNotMatch() throws Exception { | ||
internalCluster().setBootstrapMasterNodeIndex(0); | ||
String node = internalCluster().startNode(); | ||
createIndex("test-index", Settings.builder().put(FOO.getKey(), 101).put(BAR.getKey(), 102).build()); | ||
ensureYellow("test-index"); | ||
Settings dataPathSettings = internalCluster().dataPathSettings(node); | ||
ensureStableCluster(1); | ||
internalCluster().stopRandomDataNode(); | ||
|
||
Settings nodeSettings = Settings.builder().put(internalCluster().getDefaultSettings()).put(dataPathSettings).build(); | ||
UserException error = expectThrows( | ||
UserException.class, | ||
() -> removeIndexSettings(TestEnvironment.newEnvironment(nodeSettings), true, "index.not_foo") | ||
); | ||
assertThat(error.getMessage(), containsString("No index setting matching [index.not_foo] were found on this node")); | ||
internalCluster().startNode(nodeSettings); | ||
} | ||
|
||
private MockTerminal executeCommand(ElasticsearchNodeCommand command, Environment environment, boolean abort, String... args) | ||
throws Exception { | ||
final MockTerminal terminal = new MockTerminal(); | ||
final OptionSet options = command.getParser().parse(args); | ||
final String input; | ||
|
||
if (abort) { | ||
input = randomValueOtherThanMany(c -> c.equalsIgnoreCase("y"), () -> randomAlphaOfLength(1)); | ||
} else { | ||
input = randomBoolean() ? "y" : "Y"; | ||
} | ||
|
||
terminal.addTextInput(input); | ||
|
||
try { | ||
command.execute(terminal, options, environment); | ||
} finally { | ||
assertThat(terminal.getOutput(), containsString(ElasticsearchNodeCommand.STOP_WARNING_MSG)); | ||
} | ||
|
||
return terminal; | ||
} | ||
|
||
private MockTerminal removeIndexSettings(Environment environment, boolean abort, String... args) throws Exception { | ||
final MockTerminal terminal = executeCommand(new RemoveIndexSettingsCommand(), environment, abort, args); | ||
assertThat(terminal.getOutput(), containsString(RemoveIndexSettingsCommand.CONFIRMATION_MSG)); | ||
assertThat(terminal.getOutput(), containsString(RemoveIndexSettingsCommand.SETTINGS_REMOVED_MSG)); | ||
return terminal; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
100 changes: 100 additions & 0 deletions
100
server/src/main/java/org/elasticsearch/cluster/coordination/RemoveIndexSettingsCommand.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
package org.elasticsearch.cluster.coordination; | ||
|
||
import joptsimple.OptionSet; | ||
import joptsimple.OptionSpec; | ||
|
||
import org.elasticsearch.cli.ExitCodes; | ||
import org.elasticsearch.cli.Terminal; | ||
import org.elasticsearch.cli.UserException; | ||
import org.elasticsearch.cluster.ClusterState; | ||
import org.elasticsearch.cluster.metadata.IndexMetadata; | ||
import org.elasticsearch.cluster.metadata.Metadata; | ||
import org.elasticsearch.common.regex.Regex; | ||
import org.elasticsearch.common.settings.Settings; | ||
import org.elasticsearch.core.Tuple; | ||
import org.elasticsearch.env.Environment; | ||
import org.elasticsearch.gateway.PersistedClusterStateService; | ||
|
||
import java.io.IOException; | ||
import java.nio.file.Path; | ||
import java.util.List; | ||
|
||
public class RemoveIndexSettingsCommand extends ElasticsearchNodeCommand { | ||
|
||
static final String SETTINGS_REMOVED_MSG = "Index settings were successfully removed from the cluster state"; | ||
static final String CONFIRMATION_MSG = DELIMITER | ||
+ "\n" | ||
+ "You should only run this tool if you have incompatible index settings in the\n" | ||
+ "cluster state that prevent the cluster from forming.\n" | ||
+ "This tool can cause data loss and its use should be your last resort.\n" | ||
+ "\n" | ||
+ "Do you want to proceed?\n"; | ||
|
||
private final OptionSpec<String> arguments; | ||
|
||
public RemoveIndexSettingsCommand() { | ||
super("Removes index settings from the cluster state"); | ||
arguments = parser.nonOptions("index setting names"); | ||
} | ||
|
||
@Override | ||
protected void processDataPaths(Terminal terminal, Path[] dataPaths, int nodeLockId, OptionSet options, Environment env) | ||
throws IOException, UserException { | ||
final List<String> settingsToRemove = arguments.values(options); | ||
if (settingsToRemove.isEmpty()) { | ||
throw new UserException(ExitCodes.USAGE, "Must supply at least one index setting to remove"); | ||
} | ||
|
||
final PersistedClusterStateService persistedClusterStateService = createPersistedClusterStateService(env.settings(), dataPaths); | ||
|
||
terminal.println(Terminal.Verbosity.VERBOSE, "Loading cluster state"); | ||
final Tuple<Long, ClusterState> termAndClusterState = loadTermAndClusterState(persistedClusterStateService, env); | ||
final ClusterState oldClusterState = termAndClusterState.v2(); | ||
final Metadata.Builder newMetadataBuilder = Metadata.builder(oldClusterState.metadata()); | ||
int changes = 0; | ||
for (IndexMetadata indexMetadata : oldClusterState.metadata()) { | ||
Settings oldSettings = indexMetadata.getSettings(); | ||
Settings.Builder newSettings = Settings.builder().put(oldSettings); | ||
boolean removed = false; | ||
for (String settingToRemove : settingsToRemove) { | ||
for (String settingKey : oldSettings.keySet()) { | ||
if (Regex.simpleMatch(settingToRemove, settingKey)) { | ||
terminal.println( | ||
"Index setting [" + settingKey + "] will be removed from index [" + indexMetadata.getIndex() + "]" | ||
); | ||
newSettings.remove(settingKey); | ||
removed = true; | ||
} | ||
} | ||
} | ||
if (removed) { | ||
newMetadataBuilder.put(IndexMetadata.builder(indexMetadata).settings(newSettings)); | ||
changes++; | ||
} | ||
} | ||
if (changes == 0) { | ||
throw new UserException(ExitCodes.USAGE, "No index setting matching " + settingsToRemove + " were found on this node"); | ||
} | ||
|
||
final ClusterState newClusterState = ClusterState.builder(oldClusterState).metadata(newMetadataBuilder).build(); | ||
terminal.println( | ||
Terminal.Verbosity.VERBOSE, | ||
"[old cluster state = " + oldClusterState + ", new cluster state = " + newClusterState + "]" | ||
); | ||
|
||
confirm(terminal, CONFIRMATION_MSG); | ||
|
||
try (PersistedClusterStateService.Writer writer = persistedClusterStateService.createWriter()) { | ||
writer.writeFullStateAndCommit(termAndClusterState.v1(), newClusterState); | ||
} | ||
|
||
terminal.println(SETTINGS_REMOVED_MSG); | ||
} | ||
} |