Allow keystore add to handle multiple settings (#54229)

Today the keystore add command can only handle adding a single
setting/value pair in a single invocation. This incurs the startup costs
of the JVM many times, which in some environments can be expensive. This
commit teaches the add keystore command to accept adding multiple
settings in a single invocation.
This commit is contained in:
Jason Tedor 2020-03-25 22:19:45 -04:00 committed by GitHub
parent f9431001eb
commit e8e8b163cc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 110 additions and 54 deletions

View file

@ -19,20 +19,24 @@
package org.elasticsearch.common.settings; package org.elasticsearch.common.settings;
import java.io.BufferedReader;
import java.io.CharArrayWriter;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import joptsimple.OptionSet; import joptsimple.OptionSet;
import joptsimple.OptionSpec; import joptsimple.OptionSpec;
import org.elasticsearch.cli.ExitCodes; import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.Terminal; import org.elasticsearch.cli.Terminal;
import org.elasticsearch.cli.UserException; import org.elasticsearch.cli.UserException;
import org.elasticsearch.common.CheckedFunction;
import org.elasticsearch.env.Environment; import org.elasticsearch.env.Environment;
import java.io.BufferedReader;
import java.io.CharArrayWriter;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
/** /**
* A subcommand for the keystore cli which adds a string setting. * A subcommand for the keystore cli which adds a string setting.
*/ */
@ -42,13 +46,13 @@ class AddStringKeyStoreCommand extends BaseKeyStoreCommand {
private final OptionSpec<String> arguments; private final OptionSpec<String> arguments;
AddStringKeyStoreCommand() { AddStringKeyStoreCommand() {
super("Add a string setting to the keystore", false); super("Add string settings to the keystore", false);
this.stdinOption = parser.acceptsAll(Arrays.asList("x", "stdin"), "Read setting value from stdin"); this.stdinOption = parser.acceptsAll(Arrays.asList("x", "stdin"), "Read setting values from stdin");
this.forceOption = parser.acceptsAll( this.forceOption = parser.acceptsAll(
Arrays.asList("f", "force"), Arrays.asList("f", "force"),
"Overwrite existing setting without prompting, creating keystore if necessary" "Overwrite existing setting without prompting, creating keystore if necessary"
); );
this.arguments = parser.nonOptions("setting name"); this.arguments = parser.nonOptions("setting names");
} }
// pkg private so tests can manipulate // pkg private so tests can manipulate
@ -58,43 +62,53 @@ class AddStringKeyStoreCommand extends BaseKeyStoreCommand {
@Override @Override
protected void executeCommand(Terminal terminal, OptionSet options, Environment env) throws Exception { protected void executeCommand(Terminal terminal, OptionSet options, Environment env) throws Exception {
String setting = arguments.value(options); final List<String> settings = arguments.values(options);
if (setting == null) { if (settings.isEmpty()) {
throw new UserException(ExitCodes.USAGE, "The setting name can not be null"); throw new UserException(ExitCodes.USAGE, "the setting names can not be empty");
} }
final KeyStoreWrapper keyStore = getKeyStore(); final KeyStoreWrapper keyStore = getKeyStore();
if (keyStore.getSettingNames().contains(setting) && options.has(forceOption) == false) {
if (terminal.promptYesNo("Setting " + setting + " already exists. Overwrite?", false) == false) {
terminal.println("Exiting without modifying keystore.");
return;
}
}
final char[] value; final Closeable closeable;
final CheckedFunction<String, char[], IOException> valueSupplier;
if (options.has(stdinOption)) { if (options.has(stdinOption)) {
try ( final BufferedReader stdinReader = new BufferedReader(new InputStreamReader(getStdin(), StandardCharsets.UTF_8));
BufferedReader stdinReader = new BufferedReader(new InputStreamReader(getStdin(), StandardCharsets.UTF_8)); valueSupplier = s -> {
CharArrayWriter writer = new CharArrayWriter() try (CharArrayWriter writer = new CharArrayWriter()) {
) { int c;
int charInt; while ((c = stdinReader.read()) != -1) {
while ((charInt = stdinReader.read()) != -1) { if ((char) c == '\r' || (char) c == '\n') {
if ((char) charInt == '\r' || (char) charInt == '\n') { break;
break; }
writer.write((char) c);
} }
writer.write((char) charInt); return writer.toCharArray();
} }
value = writer.toCharArray(); };
} closeable = stdinReader;
} else { } else {
value = terminal.readSecret("Enter value for " + setting + ": "); valueSupplier = s -> terminal.readSecret("Enter value for " + s + ": ");
closeable = () -> {};
} }
try { try (closeable) {
keyStore.setString(setting, value); for (final String setting : settings) {
} catch (IllegalArgumentException e) { if (keyStore.getSettingNames().contains(setting) && options.has(forceOption) == false) {
throw new UserException(ExitCodes.DATA_ERROR, e.getMessage()); if (terminal.promptYesNo("Setting " + setting + " already exists. Overwrite?", false) == false) {
terminal.println("Exiting without modifying keystore.");
return;
}
}
try {
keyStore.setString(setting, valueSupplier.apply(setting));
} catch (final IllegalArgumentException e) {
throw new UserException(ExitCodes.DATA_ERROR, e.getMessage());
}
}
} }
keyStore.save(env.configFile(), getKeyStorePassword().getChars()); keyStore.save(env.configFile(), getKeyStorePassword().getChars());
} }
} }

View file

@ -19,17 +19,17 @@
package org.elasticsearch.common.settings; package org.elasticsearch.common.settings;
import org.elasticsearch.cli.Command;
import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.UserException;
import org.elasticsearch.env.Environment;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.CharArrayWriter; import java.io.CharArrayWriter;
import java.io.InputStream; import java.io.InputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Map; import java.util.Map;
import org.elasticsearch.cli.Command;
import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.UserException;
import org.elasticsearch.env.Environment;
import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasToString; import static org.hamcrest.Matchers.hasToString;
@ -155,6 +155,19 @@ public class AddStringKeyStoreCommandTests extends KeyStoreCommandTestCase {
assertSecureString("foo", "secret value", password); assertSecureString("foo", "secret value", password);
} }
public void testPromptForMultipleValues() throws Exception {
final String password = "keystorepassword";
KeyStoreWrapper.create().save(env.configFile(), password.toCharArray());
terminal.addSecretInput(password);
terminal.addSecretInput("bar1");
terminal.addSecretInput("bar2");
terminal.addSecretInput("bar3");
execute("foo1", "foo2", "foo3");
assertSecureString("foo1", "bar1", password);
assertSecureString("foo2", "bar2", password);
assertSecureString("foo3", "bar3", password);
}
public void testStdinShort() throws Exception { public void testStdinShort() throws Exception {
String password = "keystorepassword"; String password = "keystorepassword";
KeyStoreWrapper.create().save(env.configFile(), password.toCharArray()); KeyStoreWrapper.create().save(env.configFile(), password.toCharArray());
@ -200,6 +213,17 @@ public class AddStringKeyStoreCommandTests extends KeyStoreCommandTestCase {
assertSecureString("foo", "Typedthisandhitenter", password); assertSecureString("foo", "Typedthisandhitenter", password);
} }
public void testStdinWithMultipleValues() throws Exception {
final String password = "keystorepassword";
KeyStoreWrapper.create().save(env.configFile(), password.toCharArray());
terminal.addSecretInput(password);
setInput("bar1\nbar2\nbar3");
execute(randomFrom("-x", "--stdin"), "foo1", "foo2", "foo3");
assertSecureString("foo1", "bar1", password);
assertSecureString("foo2", "bar2", password);
assertSecureString("foo3", "bar3", password);
}
public void testAddUtf8String() throws Exception { public void testAddUtf8String() throws Exception {
String password = "keystorepassword"; String password = "keystorepassword";
KeyStoreWrapper.create().save(env.configFile(), password.toCharArray()); KeyStoreWrapper.create().save(env.configFile(), password.toCharArray());
@ -223,7 +247,7 @@ public class AddStringKeyStoreCommandTests extends KeyStoreCommandTestCase {
terminal.addTextInput(""); terminal.addTextInput("");
UserException e = expectThrows(UserException.class, this::execute); UserException e = expectThrows(UserException.class, this::execute);
assertEquals(ExitCodes.USAGE, e.exitCode); assertEquals(ExitCodes.USAGE, e.exitCode);
assertThat(e.getMessage(), containsString("The setting name can not be null")); assertThat(e.getMessage(), containsString("the setting names can not be empty"));
} }
public void testSpecialCharacterInName() throws Exception { public void testSpecialCharacterInName() throws Exception {

View file

@ -11,7 +11,7 @@ in the {es} keystore.
[source,shell] [source,shell]
-------------------------------------------------- --------------------------------------------------
bin/elasticsearch-keystore bin/elasticsearch-keystore
([add <setting>] [-f] [--stdin] | ([add <settings>] [-f] [--stdin] |
[add-file <setting> <path>] | [create] [-p] | [add-file <setting> <path>] | [create] [-p] |
[list] | [passwd] | [remove <setting>] | [upgrade]) [list] | [passwd] | [remove <setting>] | [upgrade])
[-h, --help] ([-s, --silent] | [-v, --verbose]) [-h, --help] ([-s, --silent] | [-v, --verbose])
@ -40,12 +40,13 @@ keystore, see the setting reference.
[[elasticsearch-keystore-parameters]] [[elasticsearch-keystore-parameters]]
=== Parameters === Parameters
`add <setting>`:: Adds settings to the keystore. By default, you are prompted `add <settings>`:: Adds settings to the keystore. Multiple setting names can be
for the value of the setting. If the keystore is password protected, you are specified as arguments to the `add` command. By default, you are prompted for
also prompted to enter the password. If the setting already exists in the the values of the settings. If the keystore is password protected, you are also
keystore, you must confirm that you want to overwrite the current value. If the prompted to enter the password. If a setting already exists in the keystore, you
keystore does not exist, you must confirm that you want to create a keystore. To must confirm that you want to overwrite the current value. If the keystore does
avoid these two confirmation prompts, use the `-f` parameter. not exist, you must confirm that you want to create a keystore. To avoid these
two confirmation prompts, use the `-f` parameter.
`add-file <setting> <path>`:: Adds a file to the keystore. `add-file <setting> <path>`:: Adds a file to the keystore.
@ -70,12 +71,14 @@ protected, you are prompted to enter the current password and the new one. You
can optionally use an empty string to remove the password. If the keystore is can optionally use an empty string to remove the password. If the keystore is
not password protected, you can use this command to set a password. not password protected, you can use this command to set a password.
`remove <setting>`:: Removes a setting from the keystore. `remove <settings>`:: Removes settings from the keystore. Multiple setting
names can be specified as arguments to the `add` command.
`-s, --silent`:: Shows minimal output. `-s, --silent`:: Shows minimal output.
`--stdin`:: When used with the `add` parameter, you can pass the setting value `--stdin`:: When used with the `add` parameter, you can pass the settings values
through standard input (stdin). See <<add-string-to-keystore>>. through standard input (stdin). Separate multiple values with carriage returns
or newlines. See <<add-string-to-keystore>>.
`upgrade`:: Upgrades the internal format of the keystore. `upgrade`:: Upgrades the internal format of the keystore.
@ -143,13 +146,28 @@ bin/elasticsearch-keystore add the.setting.name.to.set
You are prompted to enter the value of the setting. If the {es} keystore is You are prompted to enter the value of the setting. If the {es} keystore is
password protected, you are also prompted to enter the password. password protected, you are also prompted to enter the password.
To pass the setting value through standard input (stdin), use the `--stdin` flag: You can also add multiple settings with the `add` command:
[source,sh]
----------------------------------------------------------------
bin/elasticsearch-keystore add \
the.setting.name.to.set \
the.other.setting.name.to.set
----------------------------------------------------------------
You are prompted to enter the values of the settings. If the {es} keystore is
password protected, you are also prompted to enter the password.
To pass the settings values through standard input (stdin), use the `--stdin`
flag:
[source,sh] [source,sh]
---------------------------------------------------------------- ----------------------------------------------------------------
cat /file/containing/setting/value | bin/elasticsearch-keystore add --stdin the.setting.name.to.set cat /file/containing/setting/value | bin/elasticsearch-keystore add --stdin the.setting.name.to.set
---------------------------------------------------------------- ----------------------------------------------------------------
Values for multiple settings must be separated by carriage returns or newlines.
[discrete] [discrete]
[[add-file-to-keystore]] [[add-file-to-keystore]]
==== Add files to the keystore ==== Add files to the keystore