Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions docs/reference/setup/configuration.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,29 @@ file which will resolve to an environment setting, for example:
}
--------------------------------------------------

Additionally, for settings that you do not wish to store in the configuration
file, you can use the value `${prompt::text}` or `${prompt::secret}` and start
Elasticsearch in the foreground. `${prompt::secret}` has echoing disabled so
that the value entered will not be shown in your terminal; `${prompt::text}`
will allow you to see the value as you type it in. For example:

[source,yaml]
--------------------------------------------------
node:
name: ${prompt::text}
--------------------------------------------------

On execution of the `elasticsearch` command, you will be prompted to enter
the actual value like so:

[source,sh]
--------------------------------------------------
Enter value for [node.name]:
--------------------------------------------------

NOTE: Elasticsearch will not start if `${prompt::text}` or `${prompt::secret}`
is used in the settings and the process is run as a service or in the background.

The location of the configuration file can be set externally using a
system property:

Expand Down
20 changes: 15 additions & 5 deletions src/main/java/org/elasticsearch/bootstrap/Bootstrap.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.elasticsearch.Version;
import org.elasticsearch.common.PidFile;
import org.elasticsearch.common.SuppressForbidden;
import org.elasticsearch.common.cli.Terminal;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.inject.CreationException;
import org.elasticsearch.common.inject.spi.Message;
Expand Down Expand Up @@ -163,8 +164,16 @@ public void run() {

// install SM after natives, shutdown hooks, etc.
setupSecurity(settings, environment);

NodeBuilder nodeBuilder = NodeBuilder.nodeBuilder().settings(settings).loadConfigSettings(false);

// We do not need to reload system properties here as we have already applied them in building the settings and
// reloading could cause multiple prompts to the user for values if a system property was specified with a prompt
// placeholder
Settings nodeSettings = Settings.settingsBuilder()
.put(settings)
.put(InternalSettingsPreparer.IGNORE_SYSTEM_PROPERTIES_SETTING, true)
.build();

NodeBuilder nodeBuilder = NodeBuilder.nodeBuilder().settings(nodeSettings).loadConfigSettings(false);
node = nodeBuilder.build();
}

Expand Down Expand Up @@ -195,8 +204,9 @@ private static void setupLogging(Settings settings, Environment environment) {
}
}

private static Tuple<Settings, Environment> initialSettings() {
return InternalSettingsPreparer.prepareSettings(EMPTY_SETTINGS, true);
private static Tuple<Settings, Environment> initialSettings(boolean foreground) {
Terminal terminal = foreground ? Terminal.DEFAULT : null;
return InternalSettingsPreparer.prepareSettings(EMPTY_SETTINGS, true, terminal);
}

private void start() {
Expand Down Expand Up @@ -227,7 +237,7 @@ public static void main(String[] args) {
Settings settings = null;
Environment environment = null;
try {
Tuple<Settings, Environment> tuple = initialSettings();
Tuple<Settings, Environment> tuple = initialSettings(foreground);
settings = tuple.v1();
environment = tuple.v2();

Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/elasticsearch/common/cli/CliTool.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ protected CliTool(CliToolConfig config, Terminal terminal) {
Preconditions.checkArgument(config.cmds().size() != 0, "At least one command must be configured");
this.config = config;
this.terminal = terminal;
Tuple<Settings, Environment> tuple = InternalSettingsPreparer.prepareSettings(EMPTY_SETTINGS, true);
Tuple<Settings, Environment> tuple = InternalSettingsPreparer.prepareSettings(EMPTY_SETTINGS, true, terminal);
settings = tuple.v1();
env = tuple.v2();
}
Expand Down
16 changes: 14 additions & 2 deletions src/main/java/org/elasticsearch/common/settings/Settings.java
Original file line number Diff line number Diff line change
Expand Up @@ -1211,7 +1211,7 @@ public Builder putProperties(String prefix, Properties properties, String[] igno
* tries and resolve it against an environment variable ({@link System#getenv(String)}), and last, tries
* and replace it with another setting already set on this builder.
*/
public Builder replacePropertyPlaceholders() {
public Builder replacePropertyPlaceholders(String... ignoredValues) {
PropertyPlaceholder propertyPlaceholder = new PropertyPlaceholder("${", "}", false);
PropertyPlaceholder.PlaceholderResolver placeholderResolver = new PropertyPlaceholder.PlaceholderResolver() {
@Override
Expand Down Expand Up @@ -1241,7 +1241,19 @@ public boolean shouldIgnoreMissing(String placeholderName) {
}
};
for (Map.Entry<String, String> entry : Maps.newHashMap(map).entrySet()) {
String value = propertyPlaceholder.replacePlaceholders(entry.getValue(), placeholderResolver);
String possiblePlaceholder = entry.getValue();
boolean ignored = false;
for (String ignoredValue : ignoredValues) {
if (ignoredValue.equals(possiblePlaceholder)) {
ignored = true;
break;
}
}
if (ignored) {
continue;
}

String value = propertyPlaceholder.replacePlaceholders(possiblePlaceholder, placeholderResolver);
// if the values exists and has length, we should maintain it in the map
// otherwise, the replace process resolved into removing it
if (Strings.hasLength(value)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@
package org.elasticsearch.node.internal;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.UnmodifiableIterator;
import org.elasticsearch.cluster.ClusterName;
import org.elasticsearch.common.Names;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.cli.Terminal;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
Expand All @@ -41,10 +43,38 @@ public class InternalSettingsPreparer {

static final List<String> ALLOWED_SUFFIXES = ImmutableList.of(".yml", ".yaml", ".json", ".properties");

public static final String SECRET_PROMPT_VALUE = "${prompt::secret}";
public static final String TEXT_PROMPT_VALUE = "${prompt::text}";
public static final String IGNORE_SYSTEM_PROPERTIES_SETTING = "config.ignore_system_properties";

/**
* Prepares the settings by gathering all elasticsearch system properties, optionally loading the configuration settings,
* and then replacing all property placeholders. This method will not work with settings that have <code>__prompt__</code>
* as their value unless they have been resolved previously.
* @param pSettings The initial settings to use
* @param loadConfigSettings flag to indicate whether to load settings from the configuration directory/file
* @return the {@link Settings} and {@link Environment} as a {@link Tuple}
*/
public static Tuple<Settings, Environment> prepareSettings(Settings pSettings, boolean loadConfigSettings) {
return prepareSettings(pSettings, loadConfigSettings, null);
}

/**
* Prepares the settings by gathering all elasticsearch system properties, optionally loading the configuration settings,
* and then replacing all property placeholders. If a {@link Terminal} is provided and configuration settings are loaded,
* settings with the <code>__prompt__</code> value will result in a prompt for the setting to the user.
* @param pSettings The initial settings to use
* @param loadConfigSettings flag to indicate whether to load settings from the configuration directory/file
* @param terminal the Terminal to use for input/output
* @return the {@link Settings} and {@link Environment} as a {@link Tuple}
*/
public static Tuple<Settings, Environment> prepareSettings(Settings pSettings, boolean loadConfigSettings, Terminal terminal) {
// ignore this prefixes when getting properties from es. and elasticsearch.
String[] ignorePrefixes = new String[]{"es.default.", "elasticsearch.default."};
boolean useSystemProperties = !pSettings.getAsBoolean("config.ignore_system_properties", false);
// ignore the special prompt placeholders since they have the same format as property placeholders and will be resolved
// as having a default value because of the ':' in the format
String[] ignoredPlaceholders = new String[] { SECRET_PROMPT_VALUE, TEXT_PROMPT_VALUE };
boolean useSystemProperties = !pSettings.getAsBoolean(IGNORE_SYSTEM_PROPERTIES_SETTING, false);
// just create enough settings to build the environment
Settings.Builder settingsBuilder = settingsBuilder().put(pSettings);
if (useSystemProperties) {
Expand All @@ -53,7 +83,7 @@ public static Tuple<Settings, Environment> prepareSettings(Settings pSettings, b
.putProperties("elasticsearch.", System.getProperties(), ignorePrefixes)
.putProperties("es.", System.getProperties(), ignorePrefixes);
}
settingsBuilder.replacePropertyPlaceholders();
settingsBuilder.replacePropertyPlaceholders(ignoredPlaceholders);

Environment environment = new Environment(settingsBuilder.build());

Expand Down Expand Up @@ -91,17 +121,17 @@ public static Tuple<Settings, Environment> prepareSettings(Settings pSettings, b
settingsBuilder.putProperties("elasticsearch.", System.getProperties(), ignorePrefixes)
.putProperties("es.", System.getProperties(), ignorePrefixes);
}
settingsBuilder.replacePropertyPlaceholders();
settingsBuilder.replacePropertyPlaceholders(ignoredPlaceholders);

// allow to force set properties based on configuration of the settings provided
for (Map.Entry<String, String> entry : pSettings.getAsMap().entrySet()) {
String setting = entry.getKey();
if (setting.startsWith("force.")) {
settingsBuilder.remove(setting);
settingsBuilder.put(setting.substring(".force".length()), entry.getValue());
settingsBuilder.put(setting.substring("force.".length()), entry.getValue());
}
}
settingsBuilder.replacePropertyPlaceholders();
settingsBuilder.replacePropertyPlaceholders(ignoredPlaceholders);

// generate the name
if (settingsBuilder.get("name") == null) {
Expand All @@ -123,7 +153,7 @@ public static Tuple<Settings, Environment> prepareSettings(Settings pSettings, b
settingsBuilder.put(ClusterName.SETTING, ClusterName.DEFAULT.value());
}

Settings v1 = settingsBuilder.build();
Settings v1 = replacePromptPlaceholders(settingsBuilder.build(), terminal);
environment = new Environment(v1);

// put back the env settings
Expand All @@ -135,4 +165,45 @@ public static Tuple<Settings, Environment> prepareSettings(Settings pSettings, b

return new Tuple<>(v1, environment);
}

static Settings replacePromptPlaceholders(Settings settings, Terminal terminal) {
UnmodifiableIterator<Map.Entry<String, String>> iter = settings.getAsMap().entrySet().iterator();
Settings.Builder builder = Settings.builder();

while (iter.hasNext()) {
Map.Entry<String, String> entry = iter.next();
String value = entry.getValue();
String key = entry.getKey();
switch (value) {
case SECRET_PROMPT_VALUE:
String secretValue = promptForValue(key, terminal, true);
if (Strings.hasLength(secretValue)) {
builder.put(key, secretValue);
}
break;
case TEXT_PROMPT_VALUE:
String textValue = promptForValue(key, terminal, false);
if (Strings.hasLength(textValue)) {
builder.put(key, textValue);
}
break;
default:
builder.put(key, value);
break;
}
}

return builder.build();
}

static String promptForValue(String key, Terminal terminal, boolean secret) {
if (terminal == null) {
throw new UnsupportedOperationException("found property [" + key + "] with value [" + (secret ? SECRET_PROMPT_VALUE : TEXT_PROMPT_VALUE) +"]. prompting for property values is only supported when running elasticsearch in the foreground");
}

if (secret) {
return new String(terminal.readSecret("Enter value for [%s]: ", key));
}
return terminal.readText("Enter value for [%s]: ", key);
}
}
3 changes: 2 additions & 1 deletion src/main/java/org/elasticsearch/plugins/PluginManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.apache.lucene.util.IOUtils;
import org.elasticsearch.*;
import org.elasticsearch.common.SuppressForbidden;
import org.elasticsearch.common.cli.Terminal;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.http.client.HttpDownloadHelper;
import org.elasticsearch.common.io.FileSystemUtils;
Expand Down Expand Up @@ -338,7 +339,7 @@ public void listInstalledPlugins() throws IOException {
private static final int EXIT_CODE_ERROR = 70;

public static void main(String[] args) {
Tuple<Settings, Environment> initialSettings = InternalSettingsPreparer.prepareSettings(EMPTY_SETTINGS, true);
Tuple<Settings, Environment> initialSettings = InternalSettingsPreparer.prepareSettings(EMPTY_SETTINGS, true, Terminal.DEFAULT);

try {
Files.createDirectories(initialSettings.v2().pluginsFile());
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/org/elasticsearch/tribe/TribeService.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import org.elasticsearch.gateway.GatewayService;
import org.elasticsearch.node.Node;
import org.elasticsearch.node.NodeBuilder;
import org.elasticsearch.node.internal.InternalSettingsPreparer;
import org.elasticsearch.rest.RestStatus;

import java.util.EnumSet;
Expand Down Expand Up @@ -128,7 +129,7 @@ public TribeService(Settings settings, ClusterService clusterService, DiscoveryS
sb.put("node.name", settings.get("name") + "/" + entry.getKey());
sb.put("path.home", settings.get("path.home")); // pass through ES home dir
sb.put(TRIBE_NAME, entry.getKey());
sb.put("config.ignore_system_properties", true);
sb.put(InternalSettingsPreparer.IGNORE_SYSTEM_PROPERTIES_SETTING, true);
if (sb.get("http.enabled") == null) {
sb.put("http.enabled", false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.TransportAddress;
import org.elasticsearch.node.internal.InternalSettingsPreparer;
import org.elasticsearch.plugins.PluginsService;
import org.elasticsearch.test.ElasticsearchIntegrationTest;
import org.elasticsearch.test.InternalTestCluster;
Expand Down Expand Up @@ -61,7 +62,7 @@ public void testRetry() throws IOException, ExecutionException, InterruptedExcep
.put("node.mode", InternalTestCluster.nodeMode())
.put("plugins." + PluginsService.LOAD_PLUGIN_FROM_CLASSPATH, false)
.put(ClusterName.SETTING, internalCluster().getClusterName())
.put("config.ignore_system_properties", true)
.put(InternalSettingsPreparer.IGNORE_SYSTEM_PROPERTIES_SETTING, true)
.put("path.home", createTempDir());

try (TransportClient transportClient = TransportClient.builder().settings(builder.build()).build()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.TransportAddress;
import org.elasticsearch.node.Node;
import org.elasticsearch.node.internal.InternalSettingsPreparer;
import org.elasticsearch.test.ElasticsearchIntegrationTest;
import org.elasticsearch.test.ElasticsearchIntegrationTest.ClusterScope;
import org.elasticsearch.transport.TransportService;
Expand Down Expand Up @@ -59,7 +60,7 @@ public void testNodeVersionIsUpdated() {
.put("path.home", createTempDir())
.put("node.name", "testNodeVersionIsUpdated")
.put("http.enabled", false)
.put("config.ignore_system_properties", true) // make sure we get what we set :)
.put(InternalSettingsPreparer.IGNORE_SYSTEM_PROPERTIES_SETTING, true) // make sure we get what we set :)
.build()).clusterName("foobar").build();
node.start();
try {
Expand Down
45 changes: 45 additions & 0 deletions src/test/java/org/elasticsearch/common/cli/CliToolTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.node.internal.InternalSettingsPreparer;
import org.junit.Test;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

import static org.elasticsearch.common.cli.CliToolConfig.Builder.cmd;
Expand Down Expand Up @@ -271,6 +273,49 @@ public CliTool.ExitStatus execute(Settings settings, Environment env) {
tool.parse("cmd", Strings.splitStringByCommaToArray("--help"));
}

@Test
public void testPromptForSetting() throws Exception {
final AtomicInteger counter = new AtomicInteger();
final AtomicReference<String> promptedSecretValue = new AtomicReference<>(null);
final AtomicReference<String> promptedTextValue = new AtomicReference<>(null);
final Terminal terminal = new MockTerminal() {
@Override
public char[] readSecret(String text, Object... args) {
counter.incrementAndGet();
assertThat(args, arrayContaining((Object) "foo.password"));
return "changeit".toCharArray();
}

@Override
public String readText(String text, Object... args) {
counter.incrementAndGet();
assertThat(args, arrayContaining((Object) "replace"));
return "replaced";
}
};
final NamedCommand cmd = new NamedCommand("noop", terminal) {
@Override
public CliTool.ExitStatus execute(Settings settings, Environment env) {
promptedSecretValue.set(settings.get("foo.password"));
promptedTextValue.set(settings.get("replace"));
return CliTool.ExitStatus.OK;
}
};

System.setProperty("es.foo.password", InternalSettingsPreparer.SECRET_PROMPT_VALUE);
System.setProperty("es.replace", InternalSettingsPreparer.TEXT_PROMPT_VALUE);
try {
new SingleCmdTool("tool", terminal, cmd).execute();
} finally {
System.clearProperty("es.foo.password");
System.clearProperty("es.replace");
}

assertThat(counter.intValue(), is(2));
assertThat(promptedSecretValue.get(), is("changeit"));
assertThat(promptedTextValue.get(), is("replaced"));
}

private void assertStatus(int status, CliTool.ExitStatus expectedStatus) {
assertThat(status, is(expectedStatus.status()));
}
Expand Down
Loading