Skip to content

Commit c5051dc

Browse files
authored
Ignore unknown fields if overriding node metadata (#44689)
The `elasticsearch-node override-version` command fails if it cannot read the existing node metadata file. However, it reads this file strictly and fails if there are any unknown fields, which means it will not be useful if we add another field in future. This commit adds leniency to this command, allowing it to ignore any unknown fields and proceed with the downgrade. A downgrade is already unsafe, and the user is already copiously warned about this, so being lenient in this case does not make things much worse.
1 parent 534d2e5 commit c5051dc

File tree

3 files changed

+80
-14
lines changed

3 files changed

+80
-14
lines changed

server/src/main/java/org/elasticsearch/env/NodeMetaData.java

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@
3737
*/
3838
public final class NodeMetaData {
3939

40-
private static final String NODE_ID_KEY = "node_id";
41-
private static final String NODE_VERSION_KEY = "node_version";
40+
static final String NODE_ID_KEY = "node_id";
41+
static final String NODE_VERSION_KEY = "node_version";
4242

4343
private final String nodeId;
4444

@@ -71,13 +71,6 @@ public String toString() {
7171
'}';
7272
}
7373

74-
private static ObjectParser<Builder, Void> PARSER = new ObjectParser<>("node_meta_data", Builder::new);
75-
76-
static {
77-
PARSER.declareString(Builder::setNodeId, new ParseField(NODE_ID_KEY));
78-
PARSER.declareInt(Builder::setNodeVersionId, new ParseField(NODE_VERSION_KEY));
79-
}
80-
8174
public String nodeId() {
8275
return nodeId;
8376
}
@@ -130,7 +123,20 @@ public NodeMetaData build() {
130123
}
131124
}
132125

133-
public static final MetaDataStateFormat<NodeMetaData> FORMAT = new MetaDataStateFormat<NodeMetaData>("node-") {
126+
static class NodeMetaDataStateFormat extends MetaDataStateFormat<NodeMetaData> {
127+
128+
private ObjectParser<Builder, Void> objectParser;
129+
130+
/**
131+
* @param ignoreUnknownFields whether to ignore unknown fields or not. Normally we are strict about this, but
132+
* {@link OverrideNodeVersionCommand} is lenient.
133+
*/
134+
NodeMetaDataStateFormat(boolean ignoreUnknownFields) {
135+
super("node-");
136+
objectParser = new ObjectParser<>("node_meta_data", ignoreUnknownFields, Builder::new);
137+
objectParser.declareString(Builder::setNodeId, new ParseField(NODE_ID_KEY));
138+
objectParser.declareInt(Builder::setNodeVersionId, new ParseField(NODE_VERSION_KEY));
139+
}
134140

135141
@Override
136142
protected XContentBuilder newXContentBuilder(XContentType type, OutputStream stream) throws IOException {
@@ -146,8 +152,10 @@ public void toXContent(XContentBuilder builder, NodeMetaData nodeMetaData) throw
146152
}
147153

148154
@Override
149-
public NodeMetaData fromXContent(XContentParser parser) {
150-
return PARSER.apply(parser, null).build();
155+
public NodeMetaData fromXContent(XContentParser parser) throws IOException {
156+
return objectParser.apply(parser, null).build();
151157
}
152-
};
158+
}
159+
160+
public static final MetaDataStateFormat<NodeMetaData> FORMAT = new NodeMetaDataStateFormat(false);
153161
}

server/src/main/java/org/elasticsearch/env/OverrideNodeVersionCommand.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ public OverrideNodeVersionCommand() {
7373
@Override
7474
protected void processNodePaths(Terminal terminal, Path[] dataPaths, Environment env) throws IOException {
7575
final Path[] nodePaths = Arrays.stream(toNodePaths(dataPaths)).map(p -> p.path).toArray(Path[]::new);
76-
final NodeMetaData nodeMetaData = NodeMetaData.FORMAT.loadLatestState(logger, namedXContentRegistry, nodePaths);
76+
final NodeMetaData nodeMetaData
77+
= new NodeMetaData.NodeMetaDataStateFormat(true).loadLatestState(logger, namedXContentRegistry, nodePaths);
7778
if (nodeMetaData == null) {
7879
throw new ElasticsearchException(NO_METADATA_MESSAGE);
7980
}

server/src/test/java/org/elasticsearch/env/OverrideNodeVersionCommandTests.java

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,22 @@
2222
import org.elasticsearch.Version;
2323
import org.elasticsearch.cli.MockTerminal;
2424
import org.elasticsearch.common.settings.Settings;
25+
import org.elasticsearch.common.xcontent.XContentBuilder;
26+
import org.elasticsearch.common.xcontent.XContentParser;
27+
import org.elasticsearch.gateway.MetaDataStateFormat;
2528
import org.elasticsearch.gateway.WriteStateException;
2629
import org.elasticsearch.test.ESTestCase;
2730
import org.junit.Before;
2831

2932
import java.io.IOException;
3033
import java.nio.file.Path;
3134

35+
import static org.elasticsearch.env.NodeMetaData.NODE_ID_KEY;
36+
import static org.elasticsearch.env.NodeMetaData.NODE_VERSION_KEY;
3237
import static org.hamcrest.Matchers.allOf;
3338
import static org.hamcrest.Matchers.containsString;
3439
import static org.hamcrest.Matchers.equalTo;
40+
import static org.hamcrest.Matchers.hasToString;
3541

3642
public class OverrideNodeVersionCommandTests extends ESTestCase {
3743

@@ -152,4 +158,55 @@ public void testOverwritesIfTooNew() throws Exception {
152158
assertThat(nodeMetaData.nodeId(), equalTo(nodeId));
153159
assertThat(nodeMetaData.nodeVersion(), equalTo(Version.CURRENT));
154160
}
161+
162+
public void testLenientlyIgnoresExtraFields() throws Exception {
163+
final String nodeId = randomAlphaOfLength(10);
164+
final Version nodeVersion = NodeMetaDataTests.tooNewVersion();
165+
FutureNodeMetaData.FORMAT.writeAndCleanup(new FutureNodeMetaData(nodeId, nodeVersion, randomLong()), nodePaths);
166+
assertThat(expectThrows(ElasticsearchException.class,
167+
() -> NodeMetaData.FORMAT.loadLatestState(logger, xContentRegistry(), nodePaths)),
168+
hasToString(containsString("unknown field [future_field]")));
169+
170+
final MockTerminal mockTerminal = new MockTerminal();
171+
mockTerminal.addTextInput(randomFrom("y", "Y"));
172+
new OverrideNodeVersionCommand().processNodePaths(mockTerminal, nodePaths, environment);
173+
assertThat(mockTerminal.getOutput(), allOf(
174+
containsString("data loss"),
175+
containsString("You should not use this tool"),
176+
containsString(Version.CURRENT.toString()),
177+
containsString(nodeVersion.toString()),
178+
containsString(OverrideNodeVersionCommand.SUCCESS_MESSAGE)));
179+
expectThrows(IllegalStateException.class, () -> mockTerminal.readText(""));
180+
181+
final NodeMetaData nodeMetaData = NodeMetaData.FORMAT.loadLatestState(logger, xContentRegistry(), nodePaths);
182+
assertThat(nodeMetaData.nodeId(), equalTo(nodeId));
183+
assertThat(nodeMetaData.nodeVersion(), equalTo(Version.CURRENT));
184+
}
185+
186+
private static class FutureNodeMetaData {
187+
private final String nodeId;
188+
private final Version nodeVersion;
189+
private final long futureValue;
190+
191+
FutureNodeMetaData(String nodeId, Version nodeVersion, long futureValue) {
192+
this.nodeId = nodeId;
193+
this.nodeVersion = nodeVersion;
194+
this.futureValue = futureValue;
195+
}
196+
197+
static final MetaDataStateFormat<FutureNodeMetaData> FORMAT
198+
= new MetaDataStateFormat<FutureNodeMetaData>(NodeMetaData.FORMAT.getPrefix()) {
199+
@Override
200+
public void toXContent(XContentBuilder builder, FutureNodeMetaData state) throws IOException {
201+
builder.field(NODE_ID_KEY, state.nodeId);
202+
builder.field(NODE_VERSION_KEY, state.nodeVersion.id);
203+
builder.field("future_field", state.futureValue);
204+
}
205+
206+
@Override
207+
public FutureNodeMetaData fromXContent(XContentParser parser) {
208+
throw new AssertionError("shouldn't be loading a FutureNodeMetaData");
209+
}
210+
};
211+
}
155212
}

0 commit comments

Comments
 (0)