Bootstrap does not set system properties

Today, certain bootstrap properties are set and read via system
properties. This action-at-distance way of managing these properties is
rather confusing, and completely unnecessary. But another problem exists
with setting these as system properties. Namely, these system properties
are interpreted as Elasticsearch settings, not all of which are
registered. This leads to Elasticsearch failing to startup if any of
these special properties are set. Instead, these properties should be
kept as local as possible, and passed around as method parameters where
needed. This eliminates the action-at-distance way of handling these
properties, and eliminates the need to register these non-setting
properties. This commit does exactly that.

Additionally, today we use the "-D" command line flag to set the
properties, but this is confusing because "-D" is a special flag to the
JVM for setting system properties. This creates confusion because some
"-D" properties should be passed via arguments to the JVM (so via
ES_JAVA_OPTS), and some should be passed as arguments to
Elasticsearch. This commit changes the "-D" flag for Elasticsearch
settings to "-E".
This commit is contained in:
Jason Tedor 2016-03-13 09:10:56 -04:00
parent 8ac5a98b87
commit 8a05c2a2be
38 changed files with 392 additions and 429 deletions

View File

@ -407,6 +407,7 @@ class BuildPlugin implements Plugin<Project> {
systemProperty 'jna.nosys', 'true'
// default test sysprop values
systemProperty 'tests.ifNoTests', 'fail'
// TODO: remove setting logging level via system property
systemProperty 'es.logger.level', 'WARN'
for (Map.Entry<String, String> property : System.properties.entrySet()) {
if (property.getKey().startsWith('tests.') ||

View File

@ -73,6 +73,8 @@ class ClusterConfiguration {
return tmpFile.exists()
}
Map<String, String> esSettings = new HashMap<>();
Map<String, String> systemProperties = new HashMap<>()
Map<String, String> settings = new HashMap<>()
@ -86,6 +88,11 @@ class ClusterConfiguration {
LinkedHashMap<String, Object[]> setupCommands = new LinkedHashMap<>()
@Input
void esSetting(String setting, String value) {
esSettings.put(setting, value);
}
@Input
void systemProperty(String property, String value) {
systemProperties.put(property, value)

View File

@ -129,14 +129,16 @@ class NodeInfo {
'JAVA_HOME' : project.javaHome,
'ES_GC_OPTS': config.jvmArgs // we pass these with the undocumented gc opts so the argline can set gc, etc
]
args.add("-Des.node.portsfile=true")
args.addAll(config.systemProperties.collect { key, value -> "-D${key}=${value}" })
args.addAll("-E", "es.node.portsfile=true")
args.addAll(config.esSettings.collectMany { key, value -> ["-E", "${key}=${value}" ] })
env.put('ES_JAVA_OPTS', config.systemProperties.collect { key, value -> "-D${key}=${value}" }.join(" "))
for (Map.Entry<String, String> property : System.properties.entrySet()) {
if (property.getKey().startsWith('es.')) {
args.add("-D${property.getKey()}=${property.getValue()}")
args.add("-E")
args.add("${property.getKey()}=${property.getValue()}")
}
}
args.add("-Des.path.conf=${confDir}")
args.addAll("-E", "es.path.conf=${confDir}")
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
args.add('"') // end the entire command, quoted
}

View File

@ -259,7 +259,6 @@
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]action[/\\]update[/\\]UpdateRequest.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]action[/\\]update[/\\]UpdateRequestBuilder.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]bootstrap[/\\]Bootstrap.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]bootstrap[/\\]BootstrapCLIParser.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]bootstrap[/\\]JNAKernel32Library.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]bootstrap[/\\]JNANatives.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]bootstrap[/\\]JVMCheck.java" checks="LineLength" />
@ -1597,7 +1596,6 @@
<suppress files="plugins[/\\]repository-s3[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]cloud[/\\]aws[/\\]blobstore[/\\]MockDefaultS3OutputStream.java" checks="LineLength" />
<suppress files="plugins[/\\]repository-s3[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]repositories[/\\]s3[/\\]AbstractS3SnapshotRestoreTest.java" checks="LineLength" />
<suppress files="plugins[/\\]store-smb[/\\]src[/\\]main[/\\]java[/\\]org[/\\]apache[/\\]lucene[/\\]store[/\\]SmbDirectoryWrapper.java" checks="LineLength" />
<suppress files="qa[/\\]evil-tests[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]bootstrap[/\\]BootstrapCliParserTests.java" checks="LineLength" />
<suppress files="qa[/\\]evil-tests[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]bootstrap[/\\]ESPolicyUnitTests.java" checks="LineLength" />
<suppress files="qa[/\\]evil-tests[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]bootstrap[/\\]EvilSecurityTests.java" checks="LineLength" />
<suppress files="qa[/\\]evil-tests[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]common[/\\]cli[/\\]CheckFileCommandTests.java" checks="LineLength" />

View File

@ -19,21 +19,14 @@
package org.elasticsearch.bootstrap;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.file.Path;
import java.util.Locale;
import java.util.concurrent.CountDownLatch;
import org.apache.lucene.util.Constants;
import org.apache.lucene.util.IOUtils;
import org.apache.lucene.util.StringHelper;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.Version;
import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.Terminal;
import org.elasticsearch.common.PidFile;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.SuppressForbidden;
import org.elasticsearch.common.inject.CreationException;
import org.elasticsearch.common.logging.ESLogger;
@ -47,7 +40,13 @@ import org.elasticsearch.monitor.process.ProcessProbe;
import org.elasticsearch.node.Node;
import org.elasticsearch.node.internal.InternalSettingsPreparer;
import static org.elasticsearch.common.settings.Settings.Builder.EMPTY_SETTINGS;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.file.Path;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
/**
* Internal startup code.
@ -189,9 +188,14 @@ final class Bootstrap {
node = new Node(nodeSettings);
}
private static Environment initialSettings(boolean foreground) {
Terminal terminal = foreground ? Terminal.DEFAULT : null;
return InternalSettingsPreparer.prepareEnvironment(EMPTY_SETTINGS, terminal);
private static Environment initialSettings(boolean daemonize, String pathHome, String pidFile) {
Terminal terminal = daemonize ? null : Terminal.DEFAULT;
Settings.Builder builder = Settings.builder();
builder.put(Environment.PATH_HOME_SETTING.getKey(), pathHome);
if (Strings.hasLength(pidFile)) {
builder.put(Environment.PIDFILE_SETTING.getKey(), pidFile);
}
return InternalSettingsPreparer.prepareEnvironment(builder.build(), terminal);
}
private void start() {
@ -218,22 +222,19 @@ final class Bootstrap {
* This method is invoked by {@link Elasticsearch#main(String[])}
* to startup elasticsearch.
*/
static void init(String[] args) throws Throwable {
static void init(
final boolean daemonize,
final String pathHome,
final String pidFile,
final Map<String, String> esSettings) throws Throwable {
// Set the system property before anything has a chance to trigger its use
initLoggerPrefix();
BootstrapCliParser parser = new BootstrapCliParser();
int status = parser.main(args, Terminal.DEFAULT);
if (parser.shouldRun() == false || status != ExitCodes.OK) {
exit(status);
}
elasticsearchSettings(esSettings);
INSTANCE = new Bootstrap();
boolean foreground = !"false".equals(System.getProperty("es.foreground", System.getProperty("es-foreground")));
Environment environment = initialSettings(foreground);
Environment environment = initialSettings(daemonize, pathHome, pidFile);
Settings settings = environment.settings();
LogConfigurator.configure(settings, true);
checkForCustomConfFile();
@ -249,7 +250,7 @@ final class Bootstrap {
}
try {
if (!foreground) {
if (daemonize) {
Loggers.disableConsoleLogging();
closeSystOut();
}
@ -264,12 +265,12 @@ final class Bootstrap {
INSTANCE.start();
if (!foreground) {
if (daemonize) {
closeSysError();
}
} catch (Throwable e) {
// disable console logging, so user does not see the exception twice (jvm will show it already)
if (foreground) {
if (!daemonize) {
Loggers.disableConsoleLogging();
}
ESLogger logger = Loggers.getLogger(Bootstrap.class);
@ -289,7 +290,7 @@ final class Bootstrap {
logger.error("Exception", e);
}
// re-enable it if appropriate, so they can see any logging during the shutdown process
if (foreground) {
if (!daemonize) {
Loggers.enableConsoleLogging();
}
@ -297,6 +298,13 @@ final class Bootstrap {
}
}
@SuppressForbidden(reason = "Sets system properties passed as CLI parameters")
private static void elasticsearchSettings(Map<String, String> esSettings) {
for (Map.Entry<String, String> esSetting : esSettings.entrySet()) {
System.setProperty(esSetting.getKey(), esSetting.getValue());
}
}
@SuppressForbidden(reason = "System#out")
private static void closeSystOut() {
System.out.close();

View File

@ -1,95 +0,0 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.bootstrap;
import java.util.Arrays;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import org.elasticsearch.Build;
import org.elasticsearch.cli.Command;
import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.UserError;
import org.elasticsearch.common.Strings;
import org.elasticsearch.cli.Terminal;
import org.elasticsearch.common.SuppressForbidden;
import org.elasticsearch.monitor.jvm.JvmInfo;
final class BootstrapCliParser extends Command {
private final OptionSpec<Void> versionOption;
private final OptionSpec<Void> daemonizeOption;
private final OptionSpec<String> pidfileOption;
private final OptionSpec<String> propertyOption;
private boolean shouldRun = false;
BootstrapCliParser() {
super("Starts elasticsearch");
// TODO: in jopt-simple 5.0, make this mutually exclusive with all other options
versionOption = parser.acceptsAll(Arrays.asList("V", "version"),
"Prints elasticsearch version information and exits");
daemonizeOption = parser.acceptsAll(Arrays.asList("d", "daemonize"),
"Starts Elasticsearch in the background");
// TODO: in jopt-simple 5.0 this option type can be a Path
pidfileOption = parser.acceptsAll(Arrays.asList("p", "pidfile"),
"Creates a pid file in the specified path on start")
.withRequiredArg();
propertyOption = parser.accepts("D", "Configures an Elasticsearch setting")
.withRequiredArg();
}
// TODO: don't use system properties as a way to do this, its horrible...
@SuppressForbidden(reason = "Sets system properties passed as CLI parameters")
@Override
protected void execute(Terminal terminal, OptionSet options) throws Exception {
if (options.has(versionOption)) {
terminal.println("Version: " + org.elasticsearch.Version.CURRENT
+ ", Build: " + Build.CURRENT.shortHash() + "/" + Build.CURRENT.date()
+ ", JVM: " + JvmInfo.jvmInfo().version());
return;
}
// TODO: don't use sysprops for any of these! pass the args through to bootstrap...
if (options.has(daemonizeOption)) {
System.setProperty("es.foreground", "false");
}
String pidFile = pidfileOption.value(options);
if (Strings.isNullOrEmpty(pidFile) == false) {
System.setProperty("es.pidfile", pidFile);
}
for (String property : propertyOption.values(options)) {
String[] keyValue = property.split("=", 2);
if (keyValue.length != 2) {
throw new UserError(ExitCodes.USAGE, "Malformed elasticsearch setting, must be of the form key=value");
}
String key = keyValue[0];
if (key.startsWith("es.") == false) {
key = "es." + key;
}
System.setProperty(key, keyValue[1]);
}
shouldRun = true;
}
boolean shouldRun() {
return shouldRun;
}
}

View File

@ -19,23 +19,98 @@
package org.elasticsearch.bootstrap;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import joptsimple.util.KeyValuePair;
import org.elasticsearch.Build;
import org.elasticsearch.cli.Command;
import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.Terminal;
import org.elasticsearch.cli.UserError;
import org.elasticsearch.monitor.jvm.JvmInfo;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
* This class starts elasticsearch.
*/
public final class Elasticsearch {
class Elasticsearch extends Command {
private final OptionSpec<Void> versionOption;
private final OptionSpec<Void> daemonizeOption;
private final OptionSpec<String> pathHomeOption;
private final OptionSpec<String> pidfileOption;
private final OptionSpec<KeyValuePair> propertyOption;
/** no instantiation */
private Elasticsearch() {}
Elasticsearch() {
super("starts elasticsearch");
// TODO: in jopt-simple 5.0, make this mutually exclusive with all other options
versionOption = parser.acceptsAll(Arrays.asList("V", "version"),
"Prints elasticsearch version information and exits");
daemonizeOption = parser.acceptsAll(Arrays.asList("d", "daemonize"),
"Starts Elasticsearch in the background");
// TODO: in jopt-simple 5.0 this option type can be a Path
pathHomeOption = parser.acceptsAll(Arrays.asList("H", "path.home"), "").withRequiredArg();
// TODO: in jopt-simple 5.0 this option type can be a Path
pidfileOption = parser.acceptsAll(Arrays.asList("p", "pidfile"),
"Creates a pid file in the specified path on start")
.withRequiredArg();
propertyOption = parser.accepts("E", "Configure an Elasticsearch setting").withRequiredArg().ofType(KeyValuePair.class);
}
/**
* Main entry point for starting elasticsearch
*/
public static void main(String[] args) throws Exception {
public static void main(final String[] args) throws Exception {
final Elasticsearch elasticsearch = new Elasticsearch();
int status = main(args, elasticsearch, Terminal.DEFAULT);
if (status != ExitCodes.OK) {
exit(status);
}
}
static int main(final String[] args, final Elasticsearch elasticsearch, final Terminal terminal) throws Exception {
return elasticsearch.main(args, terminal);
}
@Override
protected void execute(Terminal terminal, OptionSet options) throws Exception {
if (options.has(versionOption)) {
if (options.has(daemonizeOption) || options.has(pathHomeOption) || options.has(pidfileOption)) {
throw new UserError(ExitCodes.USAGE, "Elasticsearch version option is mutually exclusive with any other option");
}
terminal.println("Version: " + org.elasticsearch.Version.CURRENT
+ ", Build: " + Build.CURRENT.shortHash() + "/" + Build.CURRENT.date()
+ ", JVM: " + JvmInfo.jvmInfo().version());
return;
}
final boolean daemonize = options.has(daemonizeOption);
final String pathHome = pathHomeOption.value(options);
final String pidFile = pidfileOption.value(options);
final Map<String, String> esSettings = new HashMap<>();
for (final KeyValuePair kvp : propertyOption.values(options)) {
if (!kvp.key.startsWith("es.")) {
throw new UserError(ExitCodes.USAGE, "Elasticsearch settings must be prefixed with [es.] but was [" + kvp.key + "]");
}
if (kvp.value.isEmpty()) {
throw new UserError(ExitCodes.USAGE, "Elasticsearch setting [" + kvp.key + "] must not be empty");
}
esSettings.put(kvp.key, kvp.value);
}
init(daemonize, pathHome, pidFile, esSettings);
}
void init(final boolean daemonize, final String pathHome, final String pidFile, final Map<String, String> esSettings) {
try {
Bootstrap.init(args);
} catch (Throwable t) {
Bootstrap.init(daemonize, pathHome, pidFile, esSettings);
} catch (final Throwable t) {
// format exceptions to the console in a special way
// to avoid 2MB stacktraces from guice, etc.
throw new StartupError(t);

View File

@ -110,9 +110,7 @@ public class LogConfigurator {
if (resolveConfig) {
resolveConfig(environment, settingsBuilder);
}
settingsBuilder
.putProperties("elasticsearch.", BootstrapInfo.getSystemProperties())
.putProperties("es.", BootstrapInfo.getSystemProperties());
settingsBuilder.putProperties("es.", BootstrapInfo.getSystemProperties());
// add custom settings after config was added so that they are not overwritten by config
settingsBuilder.put(settings);
settingsBuilder.replacePropertyPlaceholders();

View File

@ -1136,10 +1136,10 @@ public final class Settings implements ToXContent {
* @param properties The properties to put
* @return The builder
*/
public Builder putProperties(String prefix, Dictionary<Object,Object> properties) {
for (Object key1 : Collections.list(properties.keys())) {
String key = Objects.toString(key1);
String value = Objects.toString(properties.get(key));
public Builder putProperties(String prefix, Dictionary<Object, Object> properties) {
for (Object property : Collections.list(properties.keys())) {
String key = Objects.toString(property);
String value = Objects.toString(properties.get(property));
if (key.startsWith(prefix)) {
map.put(key.substring(prefix.length()), value);
}
@ -1154,19 +1154,12 @@ public final class Settings implements ToXContent {
* @param properties The properties to put
* @return The builder
*/
public Builder putProperties(String prefix, Dictionary<Object,Object> properties, String[] ignorePrefixes) {
for (Object key1 : Collections.list(properties.keys())) {
String key = Objects.toString(key1);
String value = Objects.toString(properties.get(key));
public Builder putProperties(String prefix, Dictionary<Object, Object> properties, String ignorePrefix) {
for (Object property : Collections.list(properties.keys())) {
String key = Objects.toString(property);
String value = Objects.toString(properties.get(property));
if (key.startsWith(prefix)) {
boolean ignore = false;
for (String ignorePrefix : ignorePrefixes) {
if (key.startsWith(ignorePrefix)) {
ignore = true;
break;
}
}
if (!ignore) {
if (!key.startsWith(ignorePrefix)) {
map.put(key.substring(prefix.length()), value);
}
}

View File

@ -53,8 +53,8 @@ import static org.elasticsearch.common.settings.Settings.settingsBuilder;
public class InternalSettingsPreparer {
private static final String[] ALLOWED_SUFFIXES = {".yml", ".yaml", ".json", ".properties"};
static final String[] PROPERTY_PREFIXES = {"es.", "elasticsearch."};
static final String[] PROPERTY_DEFAULTS_PREFIXES = {"es.default.", "elasticsearch.default."};
static final String PROPERTY_PREFIX = "es.";
static final String PROPERTY_DEFAULTS_PREFIX = "es.default.";
public static final String SECRET_PROMPT_VALUE = "${prompt.secret}";
public static final String TEXT_PROMPT_VALUE = "${prompt.text}";
@ -126,13 +126,9 @@ public class InternalSettingsPreparer {
output.put(input);
if (useSystemProperties(input)) {
if (loadDefaults) {
for (String prefix : PROPERTY_DEFAULTS_PREFIXES) {
output.putProperties(prefix, BootstrapInfo.getSystemProperties());
}
}
for (String prefix : PROPERTY_PREFIXES) {
output.putProperties(prefix, BootstrapInfo.getSystemProperties(), PROPERTY_DEFAULTS_PREFIXES);
output.putProperties(PROPERTY_DEFAULTS_PREFIX, BootstrapInfo.getSystemProperties());
}
output.putProperties(PROPERTY_PREFIX, BootstrapInfo.getSystemProperties(), PROPERTY_DEFAULTS_PREFIX);
}
output.replacePropertyPlaceholders();
}

View File

@ -0,0 +1,195 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.bootstrap;
import org.elasticsearch.Build;
import org.elasticsearch.Version;
import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.MockTerminal;
import org.elasticsearch.common.SuppressForbidden;
import org.elasticsearch.monitor.jvm.JvmInfo;
import org.elasticsearch.test.ESTestCase;
import org.junit.After;
import org.junit.Before;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.Matchers.hasEntry;
public class ElasticsearchCommandLineParsingTests extends ESTestCase {
public void testVersion() throws Exception {
runTestThatVersionIsMutuallyExclusiveToOtherOptions("-V", "-d");
runTestThatVersionIsMutuallyExclusiveToOtherOptions("-V", "--daemonize");
runTestThatVersionIsMutuallyExclusiveToOtherOptions("-V", "-H", "/tmp/home");
runTestThatVersionIsMutuallyExclusiveToOtherOptions("-V", "--path.home", "/tmp/home");
runTestThatVersionIsMutuallyExclusiveToOtherOptions("-V", "-p", "/tmp/pid");
runTestThatVersionIsMutuallyExclusiveToOtherOptions("-V", "--pidfile", "/tmp/pid");
runTestThatVersionIsMutuallyExclusiveToOtherOptions("--version", "-d");
runTestThatVersionIsMutuallyExclusiveToOtherOptions("--version", "--daemonize");
runTestThatVersionIsMutuallyExclusiveToOtherOptions("--version", "-H", "/tmp/home");
runTestThatVersionIsMutuallyExclusiveToOtherOptions("--version", "--path.home", "/tmp/home");
runTestThatVersionIsMutuallyExclusiveToOtherOptions("--version", "-p", "/tmp/pid");
runTestThatVersionIsMutuallyExclusiveToOtherOptions("--version", "--pidfile", "/tmp/pid");
runTestThatVersionIsReturned("-V");
runTestThatVersionIsReturned("--version");
}
private void runTestThatVersionIsMutuallyExclusiveToOtherOptions(String... args) throws Exception {
runTestVersion(
ExitCodes.USAGE,
output -> assertThat(
output,
containsString("ERROR: Elasticsearch version option is mutually exclusive with any other option")),
args);
}
private void runTestThatVersionIsReturned(String... args) throws Exception {
runTestVersion(ExitCodes.OK, output -> {
assertThat(output, containsString("Version: " + Version.CURRENT.toString()));
assertThat(output, containsString("Build: " + Build.CURRENT.shortHash() + "/" + Build.CURRENT.date()));
assertThat(output, containsString("JVM: " + JvmInfo.jvmInfo().version()));
}, args);
}
private void runTestVersion(int expectedStatus, Consumer<String> outputConsumer, String... args) throws Exception {
runTest(expectedStatus, false, outputConsumer, (daemonize, pathHome, pidFile, esSettings) -> {}, args);
}
public void testThatPidFileCanBeConfigured() throws Exception {
runPidFileTest(ExitCodes.USAGE, false, output -> assertThat(output, containsString("Option p/pidfile requires an argument")), "-p");
runPidFileTest(ExitCodes.OK, true, output -> {}, "-p", "/tmp/pid");
runPidFileTest(ExitCodes.OK, true, output -> {}, "--pidfile", "/tmp/pid");
}
private void runPidFileTest(final int expectedStatus, final boolean expectedInit, Consumer<String> outputConsumer, final String... args)
throws Exception {
runTest(
expectedStatus,
expectedInit,
outputConsumer,
(daemonize, pathHome, pidFile, esSettings) -> assertThat(pidFile, equalTo("/tmp/pid")),
args);
}
public void testThatParsingDaemonizeWorks() throws Exception {
runDaemonizeTest(true, "-d");
runDaemonizeTest(true, "--daemonize");
runDaemonizeTest(false);
}
private void runDaemonizeTest(final boolean expectedDaemonize, final String... args) throws Exception {
runTest(
ExitCodes.OK,
true,
output -> {},
(daemonize, pathHome, pidFile, esSettings) -> assertThat(daemonize, equalTo(expectedDaemonize)),
args);
}
public void testElasticsearchSettings() throws Exception {
runTest(
ExitCodes.OK,
true,
output -> {},
(daemonize, pathHome, pidFile, esSettings) -> {
assertThat(esSettings.size(), equalTo(2));
assertThat(esSettings, hasEntry("es.foo", "bar"));
assertThat(esSettings, hasEntry("es.baz", "qux"));
},
"-Ees.foo=bar", "-E", "es.baz=qux"
);
}
public void testElasticsearchSettingPrefix() throws Exception {
runElasticsearchSettingPrefixTest("-E", "foo");
runElasticsearchSettingPrefixTest("-E", "foo=bar");
runElasticsearchSettingPrefixTest("-E", "=bar");
}
private void runElasticsearchSettingPrefixTest(String... args) throws Exception {
runTest(
ExitCodes.USAGE,
false,
output -> assertThat(output, containsString("Elasticsearch settings must be prefixed with [es.] but was [")),
(daemonize, pathHome, pidFile, esSettings) -> {},
args
);
}
public void testElasticsearchSettingCanNotBeEmpty() throws Exception {
runTest(
ExitCodes.USAGE,
false,
output -> assertThat(output, containsString("Elasticsearch setting [es.foo] must not be empty")),
(daemonize, pathHome, pidFile, esSettings) -> {},
"-E", "es.foo="
);
}
public void testUnknownOption() throws Exception {
runTest(
ExitCodes.USAGE,
false,
output -> assertThat(output, containsString("network.host is not a recognized option")),
(daemonize, pathHome, pidFile, esSettings) -> {},
"--network.host");
}
private interface InitConsumer {
void accept(final boolean daemonize, final String pathHome, final String pidFile, final Map<String, String> esSettings);
}
private void runTest(
final int expectedStatus,
final boolean expectedInit,
final Consumer<String> outputConsumer,
final InitConsumer initConsumer,
String... args) throws Exception {
final MockTerminal terminal = new MockTerminal();
try {
final AtomicBoolean init = new AtomicBoolean();
final int status = Elasticsearch.main(args, new Elasticsearch() {
@Override
void init(final boolean daemonize, final String pathHome, final String pidFile, final Map<String, String> esSettings) {
init.set(true);
initConsumer.accept(daemonize, pathHome, pidFile, esSettings);
}
}, terminal);
assertThat(status, equalTo(expectedStatus));
assertThat(init.get(), equalTo(expectedInit));
outputConsumer.accept(terminal.getOutput());
} catch (Throwable t) {
// if an unexpected exception is thrown, we log
// terminal output to aid debugging
logger.info(terminal.getOutput());
// rethrow so the test fails
throw t;
}
}
}

View File

@ -1,4 +1,4 @@
# you can override this using by setting a system property, for example -Des.logger.level=DEBUG
# you can override this using by setting a system property, for example -Ees.logger.level=DEBUG
es.logger.level: INFO
rootLogger: ${es.logger.level}, console
logger:

View File

@ -99,7 +99,7 @@ fi
# Define other required variables
PID_FILE="$PID_DIR/$NAME.pid"
DAEMON=$ES_HOME/bin/elasticsearch
DAEMON_OPTS="-d -p $PID_FILE -D es.default.path.home=$ES_HOME -D es.default.path.logs=$LOG_DIR -D es.default.path.data=$DATA_DIR -D es.default.path.conf=$CONF_DIR"
DAEMON_OPTS="-d -p $PID_FILE -Ees.default.path.home=$ES_HOME -Ees.default.path.logs=$LOG_DIR -Ees.default.path.data=$DATA_DIR -Ees.default.path.conf=$CONF_DIR"
export ES_HEAP_SIZE
export ES_HEAP_NEWSIZE

View File

@ -117,7 +117,7 @@ start() {
cd $ES_HOME
echo -n $"Starting $prog: "
# if not running, start it up here, usually something like "daemon $exec"
daemon --user $ES_USER --pidfile $pidfile $exec -p $pidfile -d -D es.default.path.home=$ES_HOME -D es.default.path.logs=$LOG_DIR -D es.default.path.data=$DATA_DIR -D es.default.path.conf=$CONF_DIR
daemon --user $ES_USER --pidfile $pidfile $exec -p $pidfile -d -Ees.default.path.home=$ES_HOME -Ees.default.path.logs=$LOG_DIR -Ees.default.path.data=$DATA_DIR -Ees.default.path.conf=$CONF_DIR
retval=$?
echo
[ $retval -eq 0 ] && touch $lockfile

View File

@ -20,11 +20,11 @@ Group=elasticsearch
ExecStartPre=/usr/share/elasticsearch/bin/elasticsearch-systemd-pre-exec
ExecStart=/usr/share/elasticsearch/bin/elasticsearch \
-Des.pidfile=${PID_DIR}/elasticsearch.pid \
-Des.default.path.home=${ES_HOME} \
-Des.default.path.logs=${LOG_DIR} \
-Des.default.path.data=${DATA_DIR} \
-Des.default.path.conf=${CONF_DIR}
-Ees.pidfile=${PID_DIR}/elasticsearch.pid \
-Ees.default.path.home=${ES_HOME} \
-Ees.default.path.logs=${LOG_DIR} \
-Ees.default.path.data=${DATA_DIR} \
-Ees.default.path.conf=${CONF_DIR}
StandardOutput=journal
StandardError=inherit

View File

@ -126,11 +126,11 @@ export HOSTNAME
# manual parsing to find out, if process should be detached
daemonized=`echo $* | egrep -- '(^-d |-d$| -d |--daemonize$|--daemonize )'`
if [ -z "$daemonized" ] ; then
exec "$JAVA" $JAVA_OPTS $ES_JAVA_OPTS -Des.path.home="$ES_HOME" -cp "$ES_CLASSPATH" \
org.elasticsearch.bootstrap.Elasticsearch start "$@"
exec "$JAVA" $JAVA_OPTS $ES_JAVA_OPTS -cp "$ES_CLASSPATH" \
org.elasticsearch.bootstrap.Elasticsearch --path.home "$ES_HOME" "$@"
else
exec "$JAVA" $JAVA_OPTS $ES_JAVA_OPTS -Des.path.home="$ES_HOME" -cp "$ES_CLASSPATH" \
org.elasticsearch.bootstrap.Elasticsearch start "$@" <&- &
exec "$JAVA" $JAVA_OPTS $ES_JAVA_OPTS -cp "$ES_CLASSPATH" \
org.elasticsearch.bootstrap.Elasticsearch --path.home "$ES_HOME" "$@" <&- &
retval=$?
pid=$!
[ $retval -eq 0 ] || exit $retval

View File

@ -48,7 +48,7 @@ GOTO loop
SET HOSTNAME=%COMPUTERNAME%
"%JAVA_HOME%\bin\java" -client -Des.path.home="%ES_HOME%" !properties! -cp "%ES_HOME%/lib/*;" "org.elasticsearch.plugins.PluginCli" !args!
"%JAVA_HOME%\bin\java" -client -Ees.path.home="%ES_HOME%" !properties! -cp "%ES_HOME%/lib/*;" "org.elasticsearch.plugins.PluginCli" !args!
goto finally

View File

@ -104,4 +104,4 @@ ECHO additional elements via the plugin mechanism, or if code must really be 1>&
ECHO added to the main classpath, add jars to lib\, unsupported 1>&2
EXIT /B 1
)
set ES_PARAMS=-Delasticsearch -Des-foreground=yes -Des.path.home="%ES_HOME%"
set ES_PARAMS=-Delasticsearch -Ees.path.home="%ES_HOME%"

View File

@ -1,4 +1,4 @@
# you can override this using by setting a system property, for example -Des.logger.level=DEBUG
# you can override this using by setting a system property, for example -Ees.logger.level=DEBUG
es.logger.level: INFO
rootLogger: ${es.logger.level}, console, file
logger:

View File

@ -167,7 +167,7 @@ can do this as follows:
[source,sh]
---------------------
sudo bin/elasticsearch-plugin -Des.path.conf=/path/to/custom/config/dir install <plugin name>
sudo bin/elasticsearch-plugin -Ees.path.conf=/path/to/custom/config/dir install <plugin name>
---------------------
You can also set the `CONF_DIR` environment variable to the custom config

View File

@ -163,7 +163,7 @@ As mentioned previously, we can override either the cluster or node name. This c
[source,sh]
--------------------------------------------------
./elasticsearch --cluster.name my_cluster_name --node.name my_node_name
./elasticsearch -Ees.cluster.name=my_cluster_name -Ees.node.name=my_node_name
--------------------------------------------------
Also note the line marked http with information about the HTTP address (`192.168.8.112`) and port (`9200`) that our node is reachable from. By default, Elasticsearch uses port `9200` to provide access to its REST API. This port is configurable if necessary.

View File

@ -14,7 +14,7 @@ attribute as follows:
[source,sh]
------------------------
bin/elasticsearch --node.rack rack1 --node.size big <1>
bin/elasticsearch -Ees.node.rack=rack1 -Ees.node.size=big <1>
------------------------
<1> These attribute settings can also be specified in the `elasticsearch.yml` config file.

View File

@ -153,7 +153,7 @@ on startup if it is set too low.
==== Removed es.netty.gathering
Disabling Netty from using NIO gathering could be done via the escape
Disabling Netty from using NIO gathring could be done via the escape
hatch of setting the system property "es.netty.gathering" to "false".
Time has proven enabling gathering by default is a non-issue and this
non-documented setting has been removed.
@ -172,3 +172,18 @@ Two cache concurrency level settings
`indices.fielddata.cache.concurrency_level` because they no longer apply to
the cache implementation used for the request cache and the field data cache.
==== Using system properties to configure Elasticsearch
Elasticsearch can be configured by setting system properties on the
command line via `-Des.name.of.property=value.of.property`. This will be
removed in a future version of Elasticsearch. Instead, use
`-E es.name.of.setting=value.of.setting`. Note that in all cases the
name of the setting must be prefixed with `es.`.
==== Removed using double-dashes to configure Elasticsearch
Elasticsearch could previously be configured on the command line by
setting settings via `--name.of.setting value.of.setting`. This feature
has been removed. Instead, use
`-Ees.name.of.setting=value.of.setting`. Note that in all cases the
name of the setting must be prefixed with `es.`.

View File

@ -21,7 +21,7 @@ attribute called `rack_id` -- we could use any attribute name. For example:
[source,sh]
----------------------
./bin/elasticsearch --node.rack_id rack_one <1>
./bin/elasticsearch -Ees.node.rack_id=rack_one <1>
----------------------
<1> This setting could also be specified in the `elasticsearch.yml` config file.

View File

@ -233,7 +233,7 @@ Like all node settings, it can also be specified on the command line as:
[source,sh]
-----------------------
./bin/elasticsearch --path.data /var/elasticsearch/data
./bin/elasticsearch -Ees.path.data=/var/elasticsearch/data
-----------------------
TIP: When using the `.zip` or `.tar.gz` distributions, the `path.data` setting

View File

@ -67,13 +67,12 @@ There are added features when using the `elasticsearch` shell script.
The first, which was explained earlier, is the ability to easily run the
process either in the foreground or the background.
Another feature is the ability to pass `-D` or getopt long style
configuration parameters directly to the script. When set, all override
anything set using either `JAVA_OPTS` or `ES_JAVA_OPTS`. For example:
Another feature is the ability to pass `-E` configuration parameters
directly to the script. For example:
[source,sh]
--------------------------------------------------
$ bin/elasticsearch -Des.index.refresh_interval=5s --node.name=my-node
$ bin/elasticsearch -Ees.index.refresh_interval=5s -Ees.node.name=my-node
--------------------------------------------------
*************************************************************************

View File

@ -259,7 +259,7 @@ command, for example:
[source,sh]
--------------------------------------------------
$ elasticsearch -Des.network.host=10.0.0.4
$ elasticsearch -Ees.network.host=10.0.0.4
--------------------------------------------------
Another option is to set `es.default.` prefix instead of `es.` prefix,
@ -336,7 +336,7 @@ course, the above can also be set as a "collapsed" setting, for example:
[source,sh]
--------------------------------------------------
$ elasticsearch -Des.index.refresh_interval=5s
$ elasticsearch -Ees.index.refresh_interval=5s
--------------------------------------------------
All of the index level configuration can be found within each

View File

@ -80,7 +80,7 @@ To upgrade using a zip or compressed tarball:
overwrite the `config` or `data` directories.
* Either copy the files in the `config` directory from your old installation
to your new installation, or use the `--path.conf` option on the command
to your new installation, or use the `-E path.conf=` option on the command
line to point to an external config directory.
* Either copy the files in the `data` directory from your old installation

View File

@ -28,8 +28,8 @@ dependencies {
integTest {
cluster {
systemProperty 'es.script.inline', 'true'
systemProperty 'es.script.indexed', 'true'
esSetting 'es.script.inline', 'true'
esSetting 'es.script.indexed', 'true'
}
}

View File

@ -28,7 +28,7 @@ dependencies {
integTest {
cluster {
systemProperty 'es.script.inline', 'true'
systemProperty 'es.script.indexed', 'true'
esSetting 'es.script.inline', 'true'
esSetting 'es.script.indexed', 'true'
}
}

View File

@ -28,7 +28,7 @@ dependencies {
integTest {
cluster {
systemProperty 'es.script.inline', 'true'
systemProperty 'es.script.indexed', 'true'
esSetting 'es.script.inline', 'true'
esSetting 'es.script.indexed', 'true'
}
}

View File

@ -28,8 +28,8 @@ dependencies {
integTest {
cluster {
systemProperty 'es.script.inline', 'true'
systemProperty 'es.script.indexed', 'true'
esSetting 'es.script.inline', 'true'
esSetting 'es.script.indexed', 'true'
}
}

View File

@ -1,164 +0,0 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.bootstrap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import joptsimple.OptionException;
import org.elasticsearch.Build;
import org.elasticsearch.Version;
import org.elasticsearch.cli.Command;
import org.elasticsearch.cli.CommandTestCase;
import org.elasticsearch.cli.UserError;
import org.elasticsearch.common.SuppressForbidden;
import org.elasticsearch.monitor.jvm.JvmInfo;
import org.junit.After;
import org.junit.Before;
import static org.hamcrest.Matchers.is;
@SuppressForbidden(reason = "modifies system properties intentionally")
public class BootstrapCliParserTests extends CommandTestCase {
@Override
protected Command newCommand() {
return new BootstrapCliParser();
}
private List<String> propertiesToClear = new ArrayList<>();
private Map<Object, Object> properties;
@Before
public void before() {
this.properties = new HashMap<>(System.getProperties());
}
@After
public void clearProperties() {
for (String property : propertiesToClear) {
System.clearProperty(property);
}
propertiesToClear.clear();
assertEquals("properties leaked", properties, new HashMap<>(System.getProperties()));
}
void assertShouldRun(boolean shouldRun) {
BootstrapCliParser parser = (BootstrapCliParser)command;
assertEquals(shouldRun, parser.shouldRun());
}
public void testVersion() throws Exception {
String output = execute("-V");
assertTrue(output, output.contains(Version.CURRENT.toString()));
assertTrue(output, output.contains(Build.CURRENT.shortHash()));
assertTrue(output, output.contains(Build.CURRENT.date()));
assertTrue(output, output.contains(JvmInfo.jvmInfo().version()));
assertShouldRun(false);
terminal.reset();
output = execute("--version");
assertTrue(output, output.contains(Version.CURRENT.toString()));
assertTrue(output, output.contains(Build.CURRENT.shortHash()));
assertTrue(output, output.contains(Build.CURRENT.date()));
assertTrue(output, output.contains(JvmInfo.jvmInfo().version()));
assertShouldRun(false);
}
public void testPidfile() throws Exception {
registerProperties("es.pidfile");
// missing argument
OptionException e = expectThrows(OptionException.class, () -> {
execute("-p");
});
assertEquals("Option p/pidfile requires an argument", e.getMessage());
assertShouldRun(false);
// good cases
terminal.reset();
execute("--pidfile", "/tmp/pid");
assertSystemProperty("es.pidfile", "/tmp/pid");
assertShouldRun(true);
System.clearProperty("es.pidfile");
terminal.reset();
execute("-p", "/tmp/pid");
assertSystemProperty("es.pidfile", "/tmp/pid");
assertShouldRun(true);
}
public void testNoDaemonize() throws Exception {
registerProperties("es.foreground");
execute();
assertSystemProperty("es.foreground", null);
assertShouldRun(true);
}
public void testDaemonize() throws Exception {
registerProperties("es.foreground");
execute("-d");
assertSystemProperty("es.foreground", "false");
assertShouldRun(true);
System.clearProperty("es.foreground");
execute("--daemonize");
assertSystemProperty("es.foreground", "false");
assertShouldRun(true);
}
public void testConfig() throws Exception {
registerProperties("es.foo", "es.spam");
execute("-Dfoo=bar", "-Dspam=eggs");
assertSystemProperty("es.foo", "bar");
assertSystemProperty("es.spam", "eggs");
assertShouldRun(true);
}
public void testConfigMalformed() throws Exception {
UserError e = expectThrows(UserError.class, () -> {
execute("-Dfoo");
});
assertTrue(e.getMessage(), e.getMessage().contains("Malformed elasticsearch setting"));
}
public void testUnknownOption() throws Exception {
OptionException e = expectThrows(OptionException.class, () -> {
execute("--network.host");
});
assertTrue(e.getMessage(), e.getMessage().contains("network.host is not a recognized option"));
}
private void registerProperties(String ... systemProperties) {
propertiesToClear.addAll(Arrays.asList(systemProperties));
}
private void assertSystemProperty(String name, String expectedValue) throws Exception {
String msg = String.format(Locale.ROOT, "Expected property %s to be %s, terminal output was %s", name, expectedValue, terminal.getOutput());
assertThat(msg, System.getProperty(name), is(expectedValue));
}
}

View File

@ -21,6 +21,6 @@ apply plugin: 'elasticsearch.rest-test'
integTest {
cluster {
systemProperty 'es.node.ingest', 'false'
esSetting 'es.node.ingest', 'false'
}
}

View File

@ -21,6 +21,6 @@ apply plugin: 'elasticsearch.rest-test'
integTest {
cluster {
systemProperty 'es.script.inline', 'true'
esSetting 'es.script.inline', 'true'
}
}

View File

@ -303,7 +303,7 @@ run_elasticsearch_service() {
# This line is attempting to emulate the on login behavior of /usr/share/upstart/sessions/jayatana.conf
[ -f /usr/share/java/jayatanaag.jar ] && export JAVA_TOOL_OPTIONS="-javaagent:/usr/share/java/jayatanaag.jar"
# And now we can start Elasticsearch normally, in the background (-d) and with a pidfile (-p).
$timeoutCommand/tmp/elasticsearch/bin/elasticsearch $background -p /tmp/elasticsearch/elasticsearch.pid -Des.path.conf=$CONF_DIR $commandLineArgs
$timeoutCommand/tmp/elasticsearch/bin/elasticsearch $background -p /tmp/elasticsearch/elasticsearch.pid -Ees.path.conf=$CONF_DIR $commandLineArgs
BASH
[ "$status" -eq "$expectedStatus" ]
elif is_systemd; then

View File

@ -102,7 +102,7 @@ fi
echo "CONF_FILE=$CONF_FILE" >> /etc/sysconfig/elasticsearch;
fi
run_elasticsearch_service 1 -Des.default.config="$CONF_FILE"
run_elasticsearch_service 1 -Ees.default.config="$CONF_FILE"
# remove settings again otherwise cleaning up before next testrun will fail
if is_dpkg ; then
@ -408,7 +408,7 @@ fi
remove_jvm_example
local relativePath=${1:-$(readlink -m jvm-example-*.zip)}
sudo -E -u $ESPLUGIN_COMMAND_USER "$ESHOME/bin/elasticsearch-plugin" install "file://$relativePath" -Des.logger.level=DEBUG > /tmp/plugin-cli-output
sudo -E -u $ESPLUGIN_COMMAND_USER "$ESHOME/bin/elasticsearch-plugin" install "file://$relativePath" -Ees.logger.level=DEBUG > /tmp/plugin-cli-output
local loglines=$(cat /tmp/plugin-cli-output | wc -l)
if [ "$GROUP" == "TAR PLUGINS" ]; then
[ "$loglines" -gt "7" ] || {

View File

@ -1,65 +0,0 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.common.cli;
import java.io.IOException;
import org.elasticsearch.cli.MockTerminal;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.SuppressForbidden;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.StreamsUtils;
import org.junit.After;
import org.junit.Before;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.isEmptyString;
import static org.hamcrest.Matchers.not;
public abstract class CliToolTestCase extends ESTestCase {
@Before
@SuppressForbidden(reason = "sets es.default.path.home during tests")
public void setPathHome() {
System.setProperty("es.default.path.home", createTempDir().toString());
}
@After
@SuppressForbidden(reason = "clears es.default.path.home during tests")
public void clearPathHome() {
System.clearProperty("es.default.path.home");
}
public static String[] args(String command) {
if (!Strings.hasLength(command)) {
return Strings.EMPTY_ARRAY;
}
return command.split("\\s+");
}
public static void assertTerminalOutputContainsHelpFile(MockTerminal terminal, String classPath) throws IOException {
String output = terminal.getOutput();
assertThat(output, not(isEmptyString()));
String expectedDocs = StreamsUtils.copyToStringFromClasspath(classPath);
// convert to *nix newlines as MockTerminal used for tests also uses *nix newlines
expectedDocs = expectedDocs.replace("\r\n", "\n");
assertThat(output, containsString(expectedDocs));
}
}