diff --git a/distribution/tools/ansi-console/build.gradle b/distribution/tools/ansi-console/build.gradle index f0bd8a79d26f2..f386add3f238f 100644 --- a/distribution/tools/ansi-console/build.gradle +++ b/distribution/tools/ansi-console/build.gradle @@ -18,3 +18,9 @@ dependencies { api "org.fusesource.jansi:jansi:2.3.4" } +// the code and tests in this project cover console initialization +// which happens before the SecurityManager is installed +tasks.named("test").configure { + systemProperty 'tests.security.manager', 'false' +} + diff --git a/distribution/tools/ansi-console/src/main/java/org/elasticsearch/io/ansi/AnsiConsoleLoader.java b/distribution/tools/ansi-console/src/main/java/org/elasticsearch/io/ansi/AnsiConsoleLoader.java index 4356ce57e1ebc..81914e344e609 100644 --- a/distribution/tools/ansi-console/src/main/java/org/elasticsearch/io/ansi/AnsiConsoleLoader.java +++ b/distribution/tools/ansi-console/src/main/java/org/elasticsearch/io/ansi/AnsiConsoleLoader.java @@ -7,33 +7,72 @@ */ package org.elasticsearch.io.ansi; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.bootstrap.ConsoleLoader; +import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.SuppressForbidden; +import org.fusesource.jansi.Ansi; import org.fusesource.jansi.AnsiConsole; import org.fusesource.jansi.AnsiPrintStream; import org.fusesource.jansi.AnsiType; +import org.fusesource.jansi.io.AnsiOutputStream; -import java.io.PrintStream; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.nio.charset.Charset; import java.util.function.Supplier; +import static org.apache.logging.log4j.LogManager.getLogger; + /** - * Loads the({@link PrintStream} print stream) from {@link AnsiConsole} and checks whether it meets our requirements for a "Console". + * Loads the {@link AnsiConsole} and checks whether it meets our requirements for a "Console". * @see org.elasticsearch.bootstrap.ConsoleLoader */ -public class AnsiConsoleLoader implements Supplier { +public class AnsiConsoleLoader implements Supplier { + + private static final Logger logger = getLogger(AnsiConsoleLoader.class); - public PrintStream get() { + public ConsoleLoader.Console get() { final AnsiPrintStream out = AnsiConsole.out(); + return newConsole(out); + } + + // package-private for tests + static @Nullable ConsoleLoader.Console newConsole(AnsiPrintStream out) { if (isValidConsole(out)) { - return out; + return new ConsoleLoader.Console(out, () -> out.getTerminalWidth(), Ansi.isEnabled(), tryExtractPrintCharset(out)); } else { return null; } } - static boolean isValidConsole(AnsiPrintStream out) { + private static boolean isValidConsole(AnsiPrintStream out) { return out != null // cannot load stdout && out.getType() != AnsiType.Redirected // output is a pipe (etc) && out.getType() != AnsiType.Unsupported // could not determine terminal type && out.getTerminalWidth() > 0 // docker, non-terminal logs ; } + + /** + * Uses reflection on the JANSI lib in order to expose the {@code Charset} used to encode the console's print stream. + * The {@code Charset} is not otherwise exposed by the library, and this avoids replicating the charset selection logic in out code. + */ + @SuppressForbidden(reason = "Best effort exposing print stream's charset with reflection") + @Nullable + private static Charset tryExtractPrintCharset(AnsiPrintStream ansiPrintStream) { + try { + Method getOutMethod = ansiPrintStream.getClass().getDeclaredMethod("getOut"); + getOutMethod.setAccessible(true); + AnsiOutputStream ansiOutputStream = (AnsiOutputStream) getOutMethod.invoke(ansiPrintStream); + Field charsetField = ansiOutputStream.getClass().getDeclaredField("cs"); + charsetField.setAccessible(true); + return (Charset) charsetField.get(ansiOutputStream); + } catch (Throwable t) { + // has the library been upgraded and it now doesn't expose the same fields with the same names? + // is the Security Manager installed preventing the access + logger.info("Failed to detect JANSI's print stream encoding", t); + return null; + } + } } diff --git a/distribution/tools/ansi-console/src/test/java/org/elasticsearch/bootstrap/ConsoleLoaderTests.java b/distribution/tools/ansi-console/src/test/java/org/elasticsearch/bootstrap/ConsoleLoaderTests.java index e0bc5b04ec4e4..f34ddae8a2ffb 100644 --- a/distribution/tools/ansi-console/src/test/java/org/elasticsearch/bootstrap/ConsoleLoaderTests.java +++ b/distribution/tools/ansi-console/src/test/java/org/elasticsearch/bootstrap/ConsoleLoaderTests.java @@ -11,7 +11,6 @@ import org.elasticsearch.io.ansi.AnsiConsoleLoader; import org.elasticsearch.test.ESTestCase; -import java.io.PrintStream; import java.util.function.Supplier; import static org.hamcrest.Matchers.instanceOf; @@ -20,7 +19,7 @@ public class ConsoleLoaderTests extends ESTestCase { public void testBuildSupplier() { - final Supplier supplier = ConsoleLoader.buildConsoleLoader(AnsiConsoleLoader.class.getClassLoader()); + final Supplier supplier = ConsoleLoader.buildConsoleLoader(AnsiConsoleLoader.class.getClassLoader()); assertThat(supplier, notNullValue()); assertThat(supplier, instanceOf(AnsiConsoleLoader.class)); } diff --git a/distribution/tools/ansi-console/src/test/java/org/elasticsearch/io/ansi/AnsiConsoleLoaderTests.java b/distribution/tools/ansi-console/src/test/java/org/elasticsearch/io/ansi/AnsiConsoleLoaderTests.java index 4e2da7dd54480..4b40f8fa568de 100644 --- a/distribution/tools/ansi-console/src/test/java/org/elasticsearch/io/ansi/AnsiConsoleLoaderTests.java +++ b/distribution/tools/ansi-console/src/test/java/org/elasticsearch/io/ansi/AnsiConsoleLoaderTests.java @@ -8,7 +8,9 @@ package org.elasticsearch.io.ansi; +import org.elasticsearch.bootstrap.ConsoleLoader; import org.elasticsearch.test.ESTestCase; +import org.fusesource.jansi.Ansi; import org.fusesource.jansi.AnsiColors; import org.fusesource.jansi.AnsiMode; import org.fusesource.jansi.AnsiPrintStream; @@ -17,9 +19,12 @@ import org.fusesource.jansi.io.AnsiProcessor; import java.io.ByteArrayOutputStream; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; public class AnsiConsoleLoaderTests extends ESTestCase { @@ -30,35 +35,68 @@ public class AnsiConsoleLoaderTests extends ESTestCase { private static final AnsiOutputStream.IoRunnable NO_OP_RUNNABLE = () -> {}; + private static final Charset[] charsets = new Charset[] { + StandardCharsets.US_ASCII, + StandardCharsets.ISO_8859_1, + StandardCharsets.UTF_8, + StandardCharsets.UTF_16, + StandardCharsets.UTF_16LE, + StandardCharsets.UTF_16BE }; + public void testNullOutputIsNotConsole() { - assertThat(AnsiConsoleLoader.isValidConsole(null), is(false)); + assertThat(AnsiConsoleLoader.newConsole(null), nullValue()); } public void testRedirectedOutputIsNotConsole() { try (AnsiPrintStream ansiPrintStream = buildStream(AnsiType.Redirected, randomIntBetween(80, 120))) { - assertThat(AnsiConsoleLoader.isValidConsole(ansiPrintStream), is(false)); + assertThat(AnsiConsoleLoader.newConsole(ansiPrintStream), nullValue()); } } public void testUnsupportedTerminalIsNotConsole() { try (AnsiPrintStream ansiPrintStream = buildStream(AnsiType.Unsupported, randomIntBetween(80, 120))) { - assertThat(AnsiConsoleLoader.isValidConsole(ansiPrintStream), is(false)); + assertThat(AnsiConsoleLoader.newConsole(ansiPrintStream), nullValue()); } } public void testZeroWidthTerminalIsNotConsole() { try (AnsiPrintStream ansiPrintStream = buildStream(randomFrom(SUPPORTED_TERMINAL_TYPES), 0)) { - assertThat(AnsiConsoleLoader.isValidConsole(ansiPrintStream), is(false)); + assertThat(AnsiConsoleLoader.newConsole(ansiPrintStream), nullValue()); } } public void testStandardTerminalIsConsole() { + int width = randomIntBetween(40, 260); + try (AnsiPrintStream ansiPrintStream = buildStream(randomFrom(SUPPORTED_TERMINAL_TYPES), width)) { + ConsoleLoader.Console console = AnsiConsoleLoader.newConsole(ansiPrintStream); + assertThat(console, notNullValue()); + assertThat(console.width().get(), is(width)); + } + } + + public void testConsoleCharset() { + Charset charset = randomFrom(charsets); + try (AnsiPrintStream ansiPrintStream = buildStream(randomFrom(SUPPORTED_TERMINAL_TYPES), randomIntBetween(40, 260), charset)) { + ConsoleLoader.Console console = AnsiConsoleLoader.newConsole(ansiPrintStream); + assertThat(console, notNullValue()); + assertThat(console.charset(), is(charset)); + } + } + + public void testDisableANSI() { + Ansi.setEnabled(false); try (AnsiPrintStream ansiPrintStream = buildStream(randomFrom(SUPPORTED_TERMINAL_TYPES), randomIntBetween(40, 260))) { - assertThat(AnsiConsoleLoader.isValidConsole(ansiPrintStream), is(true)); + ConsoleLoader.Console console = AnsiConsoleLoader.newConsole(ansiPrintStream); + assertThat(console, notNullValue()); + assertThat(console.ansiEnabled(), is(false)); } } private AnsiPrintStream buildStream(AnsiType type, int width) { + return buildStream(type, width, randomFrom(charsets)); + } + + private AnsiPrintStream buildStream(AnsiType type, int width, Charset cs) { final ByteArrayOutputStream baos = new ByteArrayOutputStream(); final AnsiOutputStream ansiOutputStream = new AnsiOutputStream( baos, @@ -67,12 +105,11 @@ private AnsiPrintStream buildStream(AnsiType type, int width) { new AnsiProcessor(baos), type, randomFrom(AnsiColors.values()), - randomFrom(StandardCharsets.UTF_8, StandardCharsets.US_ASCII, StandardCharsets.UTF_16, StandardCharsets.ISO_8859_1), + cs, NO_OP_RUNNABLE, NO_OP_RUNNABLE, randomBoolean() ); return new AnsiPrintStream(ansiOutputStream, randomBoolean()); } - } diff --git a/server/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java b/server/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java index c57f6e1f86c21..ba94dcb965f41 100644 --- a/server/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java +++ b/server/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java @@ -301,7 +301,7 @@ static void init(final boolean foreground, final Path pidFile, final boolean qui final SecureSettings keystore = BootstrapUtil.loadSecureSettings(initialEnv); final Environment environment = createEnvironment(pidFile, keystore, initialEnv.settings(), initialEnv.configFile()); - BootstrapInfo.setConsoleOutput(getConsole(environment)); + BootstrapInfo.setConsole(getConsole(environment)); // the LogConfigurator will replace System.out and System.err with redirects to our logfile, so we need to capture // the stream objects before calling LogConfigurator to be able to close them when appropriate @@ -417,7 +417,7 @@ static void init(final boolean foreground, final Path pidFile, final boolean qui } } - private static PrintStream getConsole(Environment environment) { + private static ConsoleLoader.Console getConsole(Environment environment) { return ConsoleLoader.loadConsole(environment); } diff --git a/server/src/main/java/org/elasticsearch/bootstrap/BootstrapInfo.java b/server/src/main/java/org/elasticsearch/bootstrap/BootstrapInfo.java index 295cc00fa67e0..0b63211f2851a 100644 --- a/server/src/main/java/org/elasticsearch/bootstrap/BootstrapInfo.java +++ b/server/src/main/java/org/elasticsearch/bootstrap/BootstrapInfo.java @@ -11,7 +11,6 @@ import org.apache.lucene.util.SetOnce; import org.elasticsearch.core.SuppressForbidden; -import java.io.PrintStream; import java.util.Dictionary; import java.util.Enumeration; @@ -21,7 +20,7 @@ @SuppressForbidden(reason = "exposes read-only view of system properties") public final class BootstrapInfo { - private static final SetOnce consoleOutput = new SetOnce<>(); + private static final SetOnce console = new SetOnce<>(); /** no instantiation */ private BootstrapInfo() {} @@ -53,8 +52,8 @@ public static boolean isSystemCallFilterInstalled() { /** * Returns a reference to a stream attached to Standard Output, iff we have determined that stdout is a console (tty) */ - public static PrintStream getConsoleOutput() { - return consoleOutput.get(); + public static ConsoleLoader.Console getConsole() { + return console.get(); } /** @@ -123,8 +122,8 @@ public static Dictionary getSystemProperties() { public static void init() {} - static void setConsoleOutput(PrintStream output) { - consoleOutput.set(output); + static void setConsole(ConsoleLoader.Console console) { + BootstrapInfo.console.set(console); } } diff --git a/server/src/main/java/org/elasticsearch/bootstrap/ConsoleLoader.java b/server/src/main/java/org/elasticsearch/bootstrap/ConsoleLoader.java index 703739ceff355..8b0d914e2da3d 100644 --- a/server/src/main/java/org/elasticsearch/bootstrap/ConsoleLoader.java +++ b/server/src/main/java/org/elasticsearch/bootstrap/ConsoleLoader.java @@ -8,6 +8,7 @@ package org.elasticsearch.bootstrap; +import org.elasticsearch.core.Nullable; import org.elasticsearch.env.Environment; import java.io.IOException; @@ -16,6 +17,7 @@ import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; +import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.util.function.Supplier; @@ -28,20 +30,20 @@ public class ConsoleLoader { private static final String CONSOLE_LOADER_CLASS = "org.elasticsearch.io.ansi.AnsiConsoleLoader"; - public static PrintStream loadConsole(Environment env) { + public static Console loadConsole(Environment env) { final ClassLoader classLoader = buildClassLoader(env); - final Supplier supplier = buildConsoleLoader(classLoader); + final Supplier supplier = buildConsoleLoader(classLoader); return supplier.get(); } + public record Console(PrintStream printStream, Supplier width, Boolean ansiEnabled, @Nullable Charset charset) {} + @SuppressWarnings("unchecked") - static Supplier buildConsoleLoader(ClassLoader classLoader) { + static Supplier buildConsoleLoader(ClassLoader classLoader) { try { - final Class> cls = (Class>) classLoader.loadClass( - CONSOLE_LOADER_CLASS - ); - final Constructor> constructor = cls.getConstructor(); - final Supplier supplier = constructor.newInstance(); + final Class> cls = (Class>) classLoader.loadClass(CONSOLE_LOADER_CLASS); + final Constructor> constructor = cls.getConstructor(); + final Supplier supplier = constructor.newInstance(); return supplier; } catch (ReflectiveOperationException e) { throw new RuntimeException("Failed to load ANSI console", e); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/InitialNodeSecurityAutoConfiguration.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/InitialNodeSecurityAutoConfiguration.java index 5a2284ed21830..cdc93a9c48a6d 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/InitialNodeSecurityAutoConfiguration.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/InitialNodeSecurityAutoConfiguration.java @@ -9,10 +9,12 @@ import org.apache.log4j.LogManager; import org.apache.log4j.Logger; +import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.bulk.BackoffPolicy; import org.elasticsearch.action.support.GroupedActionListener; import org.elasticsearch.bootstrap.BootstrapInfo; +import org.elasticsearch.bootstrap.ConsoleLoader; import org.elasticsearch.client.internal.Client; import org.elasticsearch.common.Strings; import org.elasticsearch.common.util.concurrent.AbstractRunnable; @@ -21,11 +23,10 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.ssl.SSLService; import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore; -import org.elasticsearch.xpack.security.enrollment.BaseEnrollmentTokenGenerator; import org.elasticsearch.xpack.security.enrollment.InternalEnrollmentTokenGenerator; import org.elasticsearch.xpack.security.support.SecurityIndexManager; -import java.io.PrintStream; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Iterator; import java.util.Map; @@ -73,8 +74,8 @@ public static void maybeGenerateEnrollmentTokensAndElasticCredentialsOnNodeStart client ); - final PrintStream out = getConsoleOutput(); - if (out == null) { + final ConsoleLoader.Console console = getConsole(); + if (console == null) { LOGGER.info( "Auto-configuration will not generate a password for the elastic built-in superuser, as we cannot " + " determine if there is a terminal attached to the elasticsearch process. You can use the" @@ -132,7 +133,7 @@ protected void doRun() { kibanaEnrollmentToken, nodeEnrollmentToken, httpsCaFingerprint, - out + console ); }, e -> LOGGER.error("Unexpected exception during security auto-configuration", e)), 3 @@ -191,17 +192,17 @@ protected void doRun() { }); } - private static PrintStream getConsoleOutput() { - final PrintStream output = BootstrapInfo.getConsoleOutput(); - if (output == null) { + private static ConsoleLoader.Console getConsole() { + final ConsoleLoader.Console console = BootstrapInfo.getConsole(); + if (console == null) { return null; } // Check if it has been closed, try to write something so that we trigger PrintStream#ensureOpen - output.println(); - if (output.checkError()) { + console.printStream().println(); + if (console.printStream().checkError()) { return null; } - return output; + return console; } private static void outputInformationToConsole( @@ -209,73 +210,196 @@ private static void outputInformationToConsole( String kibanaEnrollmentToken, String nodeEnrollmentToken, String caCertFingerprint, - PrintStream out + ConsoleLoader.Console console ) { + // Use eye-catching pictograms to output the configuration information, but only if the + // console charset utilizes some known variation of UTF, otherwise we risk that the encoder + // cannot handle the special unicode code points and will display funky question marks instead + boolean useUnicode = StandardCharsets.UTF_8.equals(console.charset()) + || StandardCharsets.UTF_16.equals(console.charset()) + || StandardCharsets.UTF_16LE.equals(console.charset()) + || StandardCharsets.UTF_16BE.equals(console.charset()); + final String infoBullet = useUnicode ? "\u2139\uFE0F" : "->"; + final String bullet = useUnicode ? "\u2022" : "*"; + final String hyphenBullet = useUnicode ? "\u2043" : "-"; + final String errorBullet = useUnicode ? "\u274C" : "X"; + final String successBullet = useUnicode ? "\u2705" : "->"; + final String horizontalBorderLine = useUnicode ? "\u2501" : "-"; + final String boldOnANSI = console.ansiEnabled() ? "\u001B[1m" : ""; + final String boldOffANSI = console.ansiEnabled() ? "\u001B[22m" : ""; + final String cmdOn = "`"; + final String cmdOff = "`"; + final int horizontalBorderLength = console.width().get(); StringBuilder builder = new StringBuilder(); builder.append(System.lineSeparator()); builder.append(System.lineSeparator()); - builder.append("--------------------------------------------------------------------------------------------------------------"); + builder.append(System.lineSeparator()); + builder.append(System.lineSeparator()); + builder.append(horizontalBorderLine.repeat(horizontalBorderLength)); + builder.append(System.lineSeparator()); + builder.append(successBullet + " Elasticsearch security features have been automatically configured!"); + builder.append(System.lineSeparator()); + builder.append(successBullet + " Authentication is enabled and cluster connections are encrypted."); builder.append(System.lineSeparator()); builder.append(System.lineSeparator()); if (elasticPassword == null) { - builder.append("Unable to auto-generate the password for the elastic built-in superuser."); - } else if (Strings.isEmpty(elasticPassword)) { - builder.append("The generated password for the elastic built-in superuser has not been changed."); - } else { - builder.append("The generated password for the elastic built-in superuser is:"); + builder.append( + errorBullet + + " Unable to auto-generate the password for the " + + boldOnANSI + + "elastic" + + boldOffANSI + + " built-in superuser." + ); + } else if (false == Strings.isEmpty(elasticPassword)) { + builder.append( + infoBullet + + " Password for the " + + boldOnANSI + + "elastic" + + boldOffANSI + + " user (reset with " + + cmdOn + + "bin/elasticsearch-reset-password -u elastic" + + cmdOff + + "):" + ); builder.append(System.lineSeparator()); - builder.append(elasticPassword); + builder.append(" " + boldOnANSI + elasticPassword + boldOffANSI); + } + builder.append(System.lineSeparator()); + builder.append(System.lineSeparator()); + if (null != caCertFingerprint) { + builder.append(infoBullet + " HTTP CA certificate SHA-256 fingerprint:"); + builder.append(System.lineSeparator()); + builder.append(" " + boldOnANSI + caCertFingerprint + boldOffANSI); } builder.append(System.lineSeparator()); builder.append(System.lineSeparator()); + if (null != kibanaEnrollmentToken) { - builder.append("The enrollment token for Kibana instances, valid for the next "); - builder.append(BaseEnrollmentTokenGenerator.ENROLL_API_KEY_EXPIRATION_MINUTES); - builder.append(" minutes:"); + builder.append(infoBullet + " Configure Kibana to use this cluster:"); + builder.append(System.lineSeparator()); + builder.append(bullet + " Run Kibana and click the configuration link in the terminal when Kibana starts."); builder.append(System.lineSeparator()); - builder.append(kibanaEnrollmentToken); + builder.append(bullet + " Copy the following enrollment token and paste it into Kibana in your browser "); + builder.append("(valid for the next 30 minutes):"); + builder.append(System.lineSeparator()); + builder.append(" " + boldOnANSI + kibanaEnrollmentToken + boldOffANSI); } else { - builder.append("Unable to generate an enrollment token for Kibana instances."); + builder.append(errorBullet + " Unable to generate an enrollment token for Kibana instances, "); + builder.append("try invoking " + cmdOn + "bin/elasticsearch-create-enrollment-token -s kibana" + cmdOff + "."); } builder.append(System.lineSeparator()); builder.append(System.lineSeparator()); - if (nodeEnrollmentToken == null) { - builder.append("Unable to generate an enrollment token for Elasticsearch nodes."); + + // Node enrollment token + if (null == nodeEnrollmentToken) { + builder.append(errorBullet + " An enrollment token to enroll new nodes wasn't generated."); + builder.append(" To add nodes and enroll them into this cluster:"); builder.append(System.lineSeparator()); + builder.append(bullet + " On this node:"); builder.append(System.lineSeparator()); - } else if (false == Strings.isEmpty(nodeEnrollmentToken)) { - builder.append("The enrollment token for Elasticsearch instances, valid for the next "); - builder.append(BaseEnrollmentTokenGenerator.ENROLL_API_KEY_EXPIRATION_MINUTES); - builder.append(" minutes:"); + builder.append( + " " + + hyphenBullet + + " Create an enrollment token with " + + cmdOn + + "bin/elasticsearch-create-enrollment-token -s node" + + cmdOff + + "." + ); builder.append(System.lineSeparator()); - builder.append(nodeEnrollmentToken); + builder.append(" " + hyphenBullet + " Restart Elasticsearch."); builder.append(System.lineSeparator()); + builder.append(bullet + " On other nodes:"); builder.append(System.lineSeparator()); - } - if (null != caCertFingerprint) { - builder.append("The hex-encoded SHA-256 fingerprint of the generated HTTPS CA DER-encoded certificate:"); + builder.append( + " " + + hyphenBullet + + " Start Elasticsearch with " + + cmdOn + + "bin/elasticsearch --enrollment-token " + + cmdOff + + ", using the enrollment token that you generated." + ); + } else if (Strings.isEmpty(nodeEnrollmentToken)) { + builder.append(infoBullet + " Configure other nodes to join this cluster:"); builder.append(System.lineSeparator()); - builder.append(caCertFingerprint); + builder.append(bullet + " On this node:"); + builder.append(System.lineSeparator()); + builder.append( + " " + + hyphenBullet + + " Create an enrollment token with " + + cmdOn + + "bin/elasticsearch-create-enrollment-token -s node" + + cmdOff + + "." + ); builder.append(System.lineSeparator()); + builder.append( + " " + + hyphenBullet + + " Uncomment the " + + boldOnANSI + + "transport.host" + + boldOffANSI + + " setting at the end of " + + boldOnANSI + + "config/elasticsearch.yml" + + boldOffANSI + + "." + ); + builder.append(System.lineSeparator()); + builder.append(" " + hyphenBullet + " Restart Elasticsearch."); + builder.append(System.lineSeparator()); + builder.append(bullet + " On other nodes:"); + builder.append(System.lineSeparator()); + builder.append( + " " + + hyphenBullet + + " Start Elasticsearch with " + + cmdOn + + "bin/elasticsearch --enrollment-token " + + cmdOff + + ", using the enrollment token that you generated." + ); + } else { + builder.append(infoBullet + " Configure other nodes to join this cluster:"); + builder.append(System.lineSeparator()); + builder.append( + bullet + + " Copy the following enrollment token and start new Elasticsearch nodes with " + + cmdOn + + "bin/elasticsearch --enrollment-token " + + cmdOff + + " (valid for the next 30 minutes):" + ); + builder.append(System.lineSeparator()); + builder.append(" " + boldOnANSI + nodeEnrollmentToken + boldOffANSI); + builder.append(System.lineSeparator()); + builder.append(System.lineSeparator()); + builder.append(" If you're running in Docker, copy the enrollment token and run:"); + builder.append(System.lineSeparator()); + builder.append( + " " + + cmdOn + + "docker run -e \"ENROLLMENT_TOKEN=\" docker.elastic.co/elasticsearch/elasticsearch:" + + Version.CURRENT + + cmdOff + ); } + builder.append(System.lineSeparator()); - builder.append(System.lineSeparator()); - builder.append("You can complete the following actions at any time:"); - builder.append(System.lineSeparator()); - builder.append("Reset the password of the elastic built-in superuser with 'bin/elasticsearch-reset-password -u elastic'."); - builder.append(System.lineSeparator()); - builder.append(System.lineSeparator()); - builder.append("Generate an enrollment token for Kibana instances with 'bin/elasticsearch-create-enrollment-token -s kibana'."); - builder.append(System.lineSeparator()); - builder.append(System.lineSeparator()); - builder.append("Generate an enrollment token for Elasticsearch nodes with 'bin/elasticsearch-create-enrollment-token -s node'."); + builder.append(horizontalBorderLine.repeat(horizontalBorderLength)); builder.append(System.lineSeparator()); builder.append(System.lineSeparator()); - builder.append("--------------------------------------------------------------------------------------------------------------"); builder.append(System.lineSeparator()); builder.append(System.lineSeparator()); - out.println(builder); + + console.printStream().println(builder); } interface OnNodeStartedListener {