From d8109105b156ece5352c95c0b1f2095fffcb69c2 Mon Sep 17 00:00:00 2001 From: Jacek Date: Thu, 15 Apr 2021 16:23:43 +0200 Subject: [PATCH] SONAR-14583 add elasticsearch-keystore CLI utility class --- .../application/ProcessLauncherImpl.java | 72 +++++- .../application/command/JavaCommand.java | 15 ++ .../sonar/application/es/EsInstallation.java | 56 +++++ .../sonar/application/es/EsKeyStoreCli.java | 131 +++++++++++ .../application/ProcessLauncherImplTest.java | 138 ++++++++++-- .../application/es/EsKeyStoreCliTest.java | 205 ++++++++++++++++++ 6 files changed, 596 insertions(+), 21 deletions(-) create mode 100644 server/sonar-main/src/main/java/org/sonar/application/es/EsKeyStoreCli.java create mode 100644 server/sonar-main/src/test/java/org/sonar/application/es/EsKeyStoreCliTest.java diff --git a/server/sonar-main/src/main/java/org/sonar/application/ProcessLauncherImpl.java b/server/sonar-main/src/main/java/org/sonar/application/ProcessLauncherImpl.java index 3db72c9a357..657d3a693e8 100644 --- a/server/sonar-main/src/main/java/org/sonar/application/ProcessLauncherImpl.java +++ b/server/sonar-main/src/main/java/org/sonar/application/ProcessLauncherImpl.java @@ -24,10 +24,15 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Properties; import java.util.function.Supplier; import org.slf4j.Logger; @@ -38,6 +43,7 @@ import org.sonar.application.command.JavaCommand; import org.sonar.application.command.JvmOptions; import org.sonar.application.es.EsConnectorImpl; import org.sonar.application.es.EsInstallation; +import org.sonar.application.es.EsKeyStoreCli; import org.sonar.application.process.EsManagedProcess; import org.sonar.application.process.ManagedProcess; import org.sonar.application.process.ProcessCommandsManagedProcess; @@ -50,6 +56,9 @@ import static com.google.common.base.Preconditions.checkArgument; import static java.lang.String.format; import static java.util.Collections.singleton; import static java.util.Objects.requireNonNull; +import static org.sonar.application.es.EsKeyStoreCli.BOOTSTRAP_PASSWORD_PROPERTY_KEY; +import static org.sonar.application.es.EsKeyStoreCli.KEYSTORE_PASSWORD_PROPERTY_KEY; +import static org.sonar.application.es.EsKeyStoreCli.TRUSTSTORE_PASSWORD_PROPERTY_KEY; import static org.sonar.process.ProcessEntryPoint.PROPERTY_GRACEFUL_STOP_TIMEOUT_MS; import static org.sonar.process.ProcessEntryPoint.PROPERTY_PROCESS_INDEX; import static org.sonar.process.ProcessEntryPoint.PROPERTY_PROCESS_KEY; @@ -97,7 +106,8 @@ public class ProcessLauncherImpl implements ProcessLauncher { try { if (processId == ProcessId.ELASTICSEARCH) { checkArgument(esInstallation != null, "Incorrect configuration EsInstallation is null"); - EsConnectorImpl esConnector = new EsConnectorImpl(singleton(HostAndPort.fromParts(esInstallation.getHost(), esInstallation.getHttpPort()))); + EsConnectorImpl esConnector = new EsConnectorImpl(singleton(HostAndPort.fromParts(esInstallation.getHost(), + esInstallation.getHttpPort()))); return new EsManagedProcess(process, processId, esConnector); } else { ProcessCommands commands = allProcessesCommands.createAfterClean(processId.getIpcIndex()); @@ -140,16 +150,66 @@ public class ProcessLauncherImpl implements ProcessLauncher { }); } - private static void writeConfFiles(EsInstallation esInstallation) { + private void writeConfFiles(EsInstallation esInstallation) { File confDir = esInstallation.getConfDirectory(); - if (!confDir.exists() && !confDir.mkdirs()) { + + pruneElasticsearchConfDirectory(confDir); + createElasticsearchConfDirectory(confDir); + setupElasticsearchAuthentication(esInstallation); + + esInstallation.getEsYmlSettings().writeToYmlSettingsFile(esInstallation.getElasticsearchYml()); + esInstallation.getEsJvmOptions().writeToJvmOptionFile(esInstallation.getJvmOptions()); + storeElasticsearchLog4j2Properties(esInstallation); + } + + private static void pruneElasticsearchConfDirectory(File confDir) { + try { + Files.deleteIfExists(confDir.toPath()); + } catch (IOException e) { + throw new IllegalStateException("Could not delete Elasticsearch temporary conf directory", e); + } + } + + private static void createElasticsearchConfDirectory(File confDir) { + if (!confDir.mkdirs()) { String error = format("Failed to create temporary configuration directory [%s]", confDir.getAbsolutePath()); LOG.error(error); throw new IllegalStateException(error); } + } - esInstallation.getEsYmlSettings().writeToYmlSettingsFile(esInstallation.getElasticsearchYml()); - esInstallation.getEsJvmOptions().writeToJvmOptionFile(esInstallation.getJvmOptions()); + private void setupElasticsearchAuthentication(EsInstallation esInstallation) { + if (esInstallation.isSecurityEnabled()) { + EsKeyStoreCli keyStoreCli = EsKeyStoreCli.getInstance(esInstallation) + .store(BOOTSTRAP_PASSWORD_PROPERTY_KEY, esInstallation.getBootstrapPassword()); + + String esConfPath = esInstallation.getConfDirectory().getAbsolutePath(); + + Path trustStoreLocation = esInstallation.getTrustStoreLocation(); + Path keyStoreLocation = esInstallation.getKeyStoreLocation(); + if (trustStoreLocation.equals(keyStoreLocation)) { + copyFile(trustStoreLocation, Paths.get(esConfPath, trustStoreLocation.toFile().getName())); + } else { + copyFile(trustStoreLocation, Paths.get(esConfPath, trustStoreLocation.toFile().getName())); + copyFile(keyStoreLocation, Paths.get(esConfPath, keyStoreLocation.toFile().getName())); + } + + esInstallation.getTrustStorePassword().ifPresent(s -> keyStoreCli.store(TRUSTSTORE_PASSWORD_PROPERTY_KEY, s)); + esInstallation.getKeyStorePassword().ifPresent(s -> keyStoreCli.store(KEYSTORE_PASSWORD_PROPERTY_KEY, s)); + + keyStoreCli.executeWith(this::launchJava); + } + } + + private static void copyFile(Path from, Path to) { + try { + Files.copy(from, to, StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + throw new IllegalStateException("Could not copy file: " + from.toString(), e); + } + } + + private static void storeElasticsearchLog4j2Properties(EsInstallation esInstallation) { try (FileOutputStream fileOutputStream = new FileOutputStream(esInstallation.getLog4j2PropertiesLocation())) { esInstallation.getLog4j2Properties().store(fileOutputStream, "log4j2 properties file for ES bundled in SonarQube"); } catch (IOException e) { @@ -193,6 +253,8 @@ public class ProcessLauncherImpl implements ProcessLauncher { commands.addAll(javaCommand.getJvmOptions().getAll()); commands.addAll(buildClasspath(javaCommand)); commands.add(javaCommand.getClassName()); + commands.addAll(javaCommand.getParameters()); + if (javaCommand.getReadsArgumentsFromFile()) { commands.add(buildPropertiesFile(javaCommand).getAbsolutePath()); } else { diff --git a/server/sonar-main/src/main/java/org/sonar/application/command/JavaCommand.java b/server/sonar-main/src/main/java/org/sonar/application/command/JavaCommand.java index b72d7bfc53e..5f0eb48b16d 100644 --- a/server/sonar-main/src/main/java/org/sonar/application/command/JavaCommand.java +++ b/server/sonar-main/src/main/java/org/sonar/application/command/JavaCommand.java @@ -22,6 +22,7 @@ package org.sonar.application.command; import java.io.File; import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Properties; @@ -34,6 +35,7 @@ import static java.util.Objects.requireNonNull; public class JavaCommand extends AbstractCommand> { // program arguments private final Map arguments = new LinkedHashMap<>(); + private final List parameters = new LinkedList<>(); // entry point private String className; private JvmOptions jvmOptions; @@ -59,6 +61,19 @@ public class JavaCommand extends AbstractCommand getParameters() { + return parameters; + } + + public JavaCommand addParameter(@Nullable String parameter) { + if (parameter == null) { + parameters.remove(parameter); + } else { + parameters.add(parameter); + } + return this; + } + public String getClassName() { return className; } diff --git a/server/sonar-main/src/main/java/org/sonar/application/es/EsInstallation.java b/server/sonar-main/src/main/java/org/sonar/application/es/EsInstallation.java index 377765500a3..8afcf3e3923 100644 --- a/server/sonar-main/src/main/java/org/sonar/application/es/EsInstallation.java +++ b/server/sonar-main/src/main/java/org/sonar/application/es/EsInstallation.java @@ -20,14 +20,25 @@ package org.sonar.application.es; import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.Properties; import java.util.stream.Stream; +import javax.annotation.Nullable; +import org.apache.commons.lang.StringUtils; import org.sonar.application.command.EsJvmOptions; import org.sonar.core.util.stream.MoreCollectors; import org.sonar.process.Props; +import static org.sonar.process.ProcessProperties.Property.CLUSTER_ENABLED; +import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_KEYSTORE; +import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_KEYSTORE_PASSWORD; +import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_TRUSTSTORE; +import static org.sonar.process.ProcessProperties.Property.CLUSTER_ES_TRUSTSTORE_PASSWORD; +import static org.sonar.process.ProcessProperties.Property.CLUSTER_SEARCH_PASSWORD; import static org.sonar.process.ProcessProperties.Property.PATH_DATA; import static org.sonar.process.ProcessProperties.Property.PATH_HOME; import static org.sonar.process.ProcessProperties.Property.PATH_LOGS; @@ -53,6 +64,16 @@ public class EsInstallation { private String host; private int httpPort; + // ES authentication settings + private final boolean securityEnabled; + private final String bootstrapPassword; + private final Path keyStoreLocation; + private final Path trustStoreLocation; + @Nullable + private final String keyStorePassword; + @Nullable + private final String trustStorePassword; + public EsInstallation(Props props) { File sqHomeDir = props.nonNullValueAsFile(PATH_HOME.getKey()); @@ -61,6 +82,17 @@ public class EsInstallation { this.dataDirectory = buildDataDir(props); this.confDirectory = buildConfDir(props); this.logDirectory = buildLogPath(props); + + this.bootstrapPassword = props.value(CLUSTER_SEARCH_PASSWORD.getKey()); + this.securityEnabled = props.valueAsBoolean(CLUSTER_ENABLED.getKey()) && StringUtils.isNotBlank(bootstrapPassword); + this.keyStoreLocation = getPath(props.value(CLUSTER_ES_KEYSTORE.getKey())); + this.keyStorePassword = props.value(CLUSTER_ES_KEYSTORE_PASSWORD.getKey()); + this.trustStoreLocation = getPath(props.value(CLUSTER_ES_TRUSTSTORE.getKey())); + this.trustStorePassword = props.value(CLUSTER_ES_TRUSTSTORE_PASSWORD.getKey()); + } + + private static Path getPath(@Nullable String path) { + return Optional.ofNullable(path).map(Paths::get).orElse(null); } private static List buildOutdatedSearchDirs(Props props) { @@ -168,4 +200,28 @@ public class EsInstallation { this.httpPort = httpPort; return this; } + + public boolean isSecurityEnabled() { + return securityEnabled; + } + + public String getBootstrapPassword() { + return bootstrapPassword; + } + + public Path getKeyStoreLocation() { + return keyStoreLocation; + } + + public Path getTrustStoreLocation() { + return trustStoreLocation; + } + + public Optional getKeyStorePassword() { + return Optional.ofNullable(keyStorePassword); + } + + public Optional getTrustStorePassword() { + return Optional.ofNullable(trustStorePassword); + } } diff --git a/server/sonar-main/src/main/java/org/sonar/application/es/EsKeyStoreCli.java b/server/sonar-main/src/main/java/org/sonar/application/es/EsKeyStoreCli.java new file mode 100644 index 00000000000..76816f62905 --- /dev/null +++ b/server/sonar-main/src/main/java/org/sonar/application/es/EsKeyStoreCli.java @@ -0,0 +1,131 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.application.es; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import org.sonar.application.command.JavaCommand; +import org.sonar.application.command.JvmOptions; +import org.sonar.process.ProcessId; + +import static java.util.Objects.requireNonNull; + +public class EsKeyStoreCli { + public static final String BOOTSTRAP_PASSWORD_PROPERTY_KEY = "bootstrap.password"; + public static final String KEYSTORE_PASSWORD_PROPERTY_KEY = "xpack.security.transport.ssl.keystore.secure_password"; + public static final String TRUSTSTORE_PASSWORD_PROPERTY_KEY = "xpack.security.transport.ssl.truststore.secure_password"; + + private static final String MAIN_CLASS_NAME = "org.elasticsearch.common.settings.KeyStoreCli"; + + private final Map properties = new LinkedHashMap<>(); + private final JavaCommand command; + + private EsKeyStoreCli(EsInstallation esInstallation) { + String esHomeAbsolutePath = esInstallation.getHomeDirectory().getAbsolutePath(); + command = new JavaCommand(ProcessId.ELASTICSEARCH, esInstallation.getConfDirectory()) + .setClassName(MAIN_CLASS_NAME) + .setJvmOptions(new EsKeyStoreJvmOptions(esInstallation)) + .addClasspath(Paths.get(esHomeAbsolutePath, "lib").toAbsolutePath() + "/*") + .addClasspath(Paths.get(esHomeAbsolutePath, "lib", "tools", "keystore-cli") + .toAbsolutePath() + "/*") + .addParameter("add") + .addParameter("-x") + .addParameter("-f"); + } + + public static EsKeyStoreCli getInstance(EsInstallation esInstallation) { + return new EsKeyStoreCli(esInstallation); + } + + public EsKeyStoreCli store(String key, String value) { + requireNonNull(key, "Property key cannot be null"); + requireNonNull(value, "Property value cannot be null"); + properties.computeIfAbsent(key, s -> { + command.addParameter(key); + return value; + }); + return this; + } + + public Process executeWith(Function, Process> commandLauncher) { + Process process = commandLauncher.apply(command); + writeValues(process); + waitFor(process); + checkExitValue(process.exitValue()); + return process; + } + + private void writeValues(Process process) { + try (OutputStream stdin = process.getOutputStream(); + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stdin, StandardCharsets.UTF_8))) { + for (Entry entry : properties.entrySet()) { + writer.write(entry.getValue()); + writer.write("\n"); + } + writer.flush(); + + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + private static void waitFor(Process process) { + try { + process.waitFor(1, TimeUnit.MINUTES); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("EsKeyStoreCli has been interrupted", e); + } + } + + private static void checkExitValue(int code) { + if (code != 0) { + throw new IllegalStateException("Elasticsearch KeyStore tool exited with code: " + code); + } + } + + public static class EsKeyStoreJvmOptions extends JvmOptions { + + public EsKeyStoreJvmOptions(EsInstallation esInstallation) { + super(mandatoryOptions(esInstallation)); + } + + private static Map mandatoryOptions(EsInstallation esInstallation) { + Map res = new LinkedHashMap<>(7); + res.put("-Xshare:auto", ""); + res.put("-Xms4m", ""); + res.put("-Xmx64m", ""); + res.put("-Des.path.home=", esInstallation.getHomeDirectory().getAbsolutePath()); + res.put("-Des.path.conf=", esInstallation.getConfDirectory().getAbsolutePath()); + res.put("-Des.distribution=", "default"); + res.put("-Des.distribution.type=", "tar"); + return res; + } + } +} diff --git a/server/sonar-main/src/test/java/org/sonar/application/ProcessLauncherImplTest.java b/server/sonar-main/src/test/java/org/sonar/application/ProcessLauncherImplTest.java index f97b1a562f1..131626c796b 100644 --- a/server/sonar-main/src/test/java/org/sonar/application/ProcessLauncherImplTest.java +++ b/server/sonar-main/src/test/java/org/sonar/application/ProcessLauncherImplTest.java @@ -22,6 +22,7 @@ package org.sonar.application; import java.io.File; import java.io.FileReader; import java.io.IOException; +import java.nio.file.Paths; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -29,7 +30,6 @@ import java.util.Properties; import java.util.Random; import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.rules.TemporaryFolder; import org.sonar.application.command.EsJvmOptions; import org.sonar.application.command.EsScriptCommand; @@ -43,6 +43,7 @@ import org.sonar.process.Props; import org.sonar.process.sharedmemoryfile.AllProcessesCommands; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.data.MapEntry.entry; import static org.mockito.Mockito.RETURNS_MOCKS; import static org.mockito.Mockito.mock; @@ -51,11 +52,9 @@ import static org.mockito.Mockito.when; public class ProcessLauncherImplTest { @Rule - public TemporaryFolder temp = new TemporaryFolder(); - @Rule - public ExpectedException expectedException = ExpectedException.none(); + public final TemporaryFolder temp = new TemporaryFolder(); - private AllProcessesCommands commands = mock(AllProcessesCommands.class, RETURNS_MOCKS); + private final AllProcessesCommands commands = mock(AllProcessesCommands.class, RETURNS_MOCKS); @Test public void launch_forks_a_new_process() throws Exception { @@ -91,6 +90,99 @@ public class ProcessLauncherImplTest { .containsAllEntriesOf(command.getEnvVariables()); } + @Test + public void enabling_es_security_should_execute_keystore_cli_if_cert_password_provided() throws Exception { + File tempDir = temp.newFolder(); + File certificateFile = temp.newFile("certificate.pk12"); + TestProcessBuilder processBuilder = new TestProcessBuilder(); + ProcessLauncher underTest = new ProcessLauncherImpl(tempDir, commands, () -> processBuilder); + + EsInstallation esInstallation = createEsInstallation(new Props(new Properties()) + .set("sonar.cluster.enabled", "true") + .set("sonar.cluster.search.password", "bootstrap-password") + .set("sonar.cluster.es.ssl.keystore", certificateFile.getAbsolutePath()) + .set("sonar.cluster.es.ssl.keystorePassword", "keystore-password") + .set("sonar.cluster.es.ssl.truststore", certificateFile.getAbsolutePath()) + .set("sonar.cluster.es.ssl.truststorePassword", "truststore-password")); + + JavaCommand command = new JavaCommand<>(ProcessId.ELASTICSEARCH, temp.newFolder()); + command.addClasspath("lib/*.class"); + command.addClasspath("lib/*.jar"); + command.setArgument("foo", "bar"); + command.setClassName("org.sonarqube.Main"); + command.setEnvVariable("VAR1", "valueOfVar1"); + command.setJvmOptions(new JvmOptions<>() + .add("-Dfoo=bar") + .add("-Dfoo2=bar2")); + command.setEsInstallation(esInstallation); + + ManagedProcess monitor = underTest.launch(command); + assertThat(monitor).isNotNull(); + assertThat(Paths.get(esInstallation.getConfDirectory().getAbsolutePath(), "certificate.pk12")).exists(); + } + + @Test + public void enabling_es_security_should_execute_keystore_cli_if_no_cert_password_provided() throws Exception { + File tempDir = temp.newFolder(); + File certificateFile = temp.newFile("certificate.pk12"); + TestProcessBuilder processBuilder = new TestProcessBuilder(); + ProcessLauncher underTest = new ProcessLauncherImpl(tempDir, commands, () -> processBuilder); + + EsInstallation esInstallation = createEsInstallation(new Props(new Properties()) + .set("sonar.cluster.enabled", "true") + .set("sonar.cluster.search.password", "bootstrap-password") + .set("sonar.cluster.es.ssl.keystore", certificateFile.getAbsolutePath()) + .set("sonar.cluster.es.ssl.truststore", certificateFile.getAbsolutePath())); + + JavaCommand command = new JavaCommand<>(ProcessId.ELASTICSEARCH, temp.newFolder()); + command.addClasspath("lib/*.class"); + command.addClasspath("lib/*.jar"); + command.setArgument("foo", "bar"); + command.setClassName("org.sonarqube.Main"); + command.setEnvVariable("VAR1", "valueOfVar1"); + command.setJvmOptions(new JvmOptions<>() + .add("-Dfoo=bar") + .add("-Dfoo2=bar2")); + command.setEsInstallation(esInstallation); + + ManagedProcess monitor = underTest.launch(command); + assertThat(monitor).isNotNull(); + assertThat(Paths.get(esInstallation.getConfDirectory().getAbsolutePath(), "certificate.pk12")).exists(); + } + + @Test + public void enabling_es_security_should_execute_keystore_cli_if_truststore_and_keystore_provided() throws Exception { + File tempDir = temp.newFolder(); + File truststoreFile = temp.newFile("truststore.pk12"); + File keystoreFile = temp.newFile("keystore.pk12"); + TestProcessBuilder processBuilder = new TestProcessBuilder(); + ProcessLauncher underTest = new ProcessLauncherImpl(tempDir, commands, () -> processBuilder); + + EsInstallation esInstallation = createEsInstallation(new Props(new Properties()) + .set("sonar.cluster.enabled", "true") + .set("sonar.cluster.search.password", "bootstrap-password") + .set("sonar.cluster.es.ssl.keystore", keystoreFile.getAbsolutePath()) + .set("sonar.cluster.es.ssl.keystorePassword", "keystore-password") + .set("sonar.cluster.es.ssl.truststore", truststoreFile.getAbsolutePath()) + .set("sonar.cluster.es.ssl.truststorePassword", "truststore-password")); + + JavaCommand command = new JavaCommand<>(ProcessId.ELASTICSEARCH, temp.newFolder()); + command.addClasspath("lib/*.class"); + command.addClasspath("lib/*.jar"); + command.setArgument("foo", "bar"); + command.setClassName("org.sonarqube.Main"); + command.setEnvVariable("VAR1", "valueOfVar1"); + command.setJvmOptions(new JvmOptions<>() + .add("-Dfoo=bar") + .add("-Dfoo2=bar2")); + command.setEsInstallation(esInstallation); + + ManagedProcess monitor = underTest.launch(command); + assertThat(monitor).isNotNull(); + assertThat(Paths.get(esInstallation.getConfDirectory().getAbsolutePath(), "truststore.pk12")).exists(); + assertThat(Paths.get(esInstallation.getConfDirectory().getAbsolutePath(), "keystore.pk12")).exists(); + } + @Test public void properties_are_passed_to_command_via_a_temporary_properties_file() throws Exception { File tempDir = temp.newFolder(); @@ -127,7 +219,7 @@ public class ProcessLauncherImplTest { File tempDir = temp.newFolder(); TestProcessBuilder processBuilder = new TestProcessBuilder(); ProcessLauncher underTest = new ProcessLauncherImpl(tempDir, commands, () -> processBuilder); - JavaCommand command = new JavaCommand<>(ProcessId.WEB_SERVER, temp.newFolder()); + JavaCommand> command = new JavaCommand<>(ProcessId.WEB_SERVER, temp.newFolder()); command.setReadsArgumentsFromFile(false); command.setArgument("foo", "bar"); command.setArgument("baz", "woo"); @@ -146,16 +238,16 @@ public class ProcessLauncherImplTest { File homeDir = temp.newFolder(); File dataDir = temp.newFolder(); File logDir = temp.newFolder(); - ProcessLauncher underTest = new ProcessLauncherImpl(tempDir, commands, () -> new TestProcessBuilder()); + ProcessLauncher underTest = new ProcessLauncherImpl(tempDir, commands, TestProcessBuilder::new); EsScriptCommand command = createEsScriptCommand(tempDir, homeDir, dataDir, logDir); File outdatedEsDir = new File(dataDir, "es"); assertThat(outdatedEsDir.mkdir()).isTrue(); - assertThat(outdatedEsDir.exists()).isTrue(); + assertThat(outdatedEsDir).exists(); underTest.launch(command); - assertThat(outdatedEsDir.exists()).isFalse(); + assertThat(outdatedEsDir).doesNotExist(); } @Test @@ -168,11 +260,11 @@ public class ProcessLauncherImplTest { EsScriptCommand command = createEsScriptCommand(tempDir, homeDir, dataDir, logDir); File outdatedEsDir = new File(dataDir, "es"); - assertThat(outdatedEsDir.exists()).isFalse(); + assertThat(outdatedEsDir).doesNotExist(); underTest.launch(command); - assertThat(outdatedEsDir.exists()).isFalse(); + assertThat(outdatedEsDir).doesNotExist(); } @Test @@ -182,10 +274,10 @@ public class ProcessLauncherImplTest { when(processBuilder.start()).thenThrow(new IOException("error")); ProcessLauncher underTest = new ProcessLauncherImpl(tempDir, commands, () -> processBuilder); - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("Fail to launch process [es]"); - - underTest.launch(new JavaCommand(ProcessId.ELASTICSEARCH, temp.newFolder())); + JavaCommand javaCommand = new JavaCommand<>(ProcessId.ELASTICSEARCH, temp.newFolder()); + assertThatThrownBy(() -> underTest.launch(javaCommand)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Fail to launch process [es]"); } private EsScriptCommand createEsScriptCommand(File tempDir, File homeDir, File dataDir, File logDir) throws IOException { @@ -204,6 +296,20 @@ public class ProcessLauncherImplTest { return command; } + private EsInstallation createEsInstallation(Props props) throws IOException { + File tempFolder = this.temp.newFolder("temp"); + return new EsInstallation(props + .set("sonar.path.home", this.temp.newFolder("home").getAbsolutePath()) + .set("sonar.path.data", this.temp.newFolder("data").getAbsolutePath()) + .set("sonar.path.temp", tempFolder.getAbsolutePath()) + .set("sonar.path.logs", this.temp.newFolder("logs").getAbsolutePath())) + .setHttpPort(9001) + .setHost("localhost") + .setEsYmlSettings(new EsYmlSettings(new HashMap<>())) + .setEsJvmOptions(new EsJvmOptions(new Props(new Properties()), tempFolder)) + .setLog4j2Properties(new Properties()); + } + private EsInstallation createEsInstallation() throws IOException { File tempFolder = this.temp.newFolder("temp"); return new EsInstallation(new Props(new Properties()) @@ -256,7 +362,7 @@ public class ProcessLauncherImplTest { @Override public Process start() { this.started = true; - return mock(Process.class); + return mock(Process.class, RETURNS_MOCKS); } } } diff --git a/server/sonar-main/src/test/java/org/sonar/application/es/EsKeyStoreCliTest.java b/server/sonar-main/src/test/java/org/sonar/application/es/EsKeyStoreCliTest.java new file mode 100644 index 00000000000..8ea404fd6a1 --- /dev/null +++ b/server/sonar-main/src/test/java/org/sonar/application/es/EsKeyStoreCliTest.java @@ -0,0 +1,205 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.application.es; + +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.concurrent.TimeUnit; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.mockito.ArgumentMatcher; +import org.sonar.application.command.JavaCommand; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class EsKeyStoreCliTest { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + EsInstallation esInstallation = mock(EsInstallation.class); + + @Test + public void execute_command_should_preserve_order_of_properties() throws Exception { + File homeDir = temp.newFolder(); + File confDir = temp.newFolder(); + when(esInstallation.getHomeDirectory()).thenReturn(homeDir); + when(esInstallation.getConfDirectory()).thenReturn(confDir); + + EsKeyStoreCli underTest = EsKeyStoreCli.getInstance(esInstallation); + underTest + .store("test.property1", "value1") + .store("test.property2", "value2") + .store("test.property3", "value3"); + + MockProcess process = (MockProcess) underTest.executeWith(EsKeyStoreCliTest::mockLaunch); + + JavaCommand executedCommand = process.getExecutedCommand(); + assertThat(executedCommand.getClassName()).isEqualTo("org.elasticsearch.common.settings.KeyStoreCli"); + assertThat(executedCommand.getClasspath()) + .containsExactly(homeDir.getAbsolutePath() + "/lib/*", homeDir.getAbsolutePath() + "/lib/tools/keystore-cli/*"); + assertThat(executedCommand.getParameters()).containsExactly("add", "-x", "-f", "test.property1", "test.property2", "test.property3"); + assertThat(executedCommand.getJvmOptions().getAll()).containsExactly( + "-Xshare:auto", + "-Xms4m", + "-Xmx64m", + "-Des.path.home=" + homeDir.getAbsolutePath(), + "-Des.path.conf=" + confDir.getAbsolutePath(), + "-Des.distribution=default", + "-Des.distribution.type=tar"); + + verify(process.getOutputStream()).write(argThat(new ArrayContainsMatcher("value1\nvalue2\nvalue3\n")), eq(0), eq(21)); + verify(process.getOutputStream()).flush(); + verify(process.getMock()).waitFor(1L, TimeUnit.MINUTES); + } + + @Test + public void ISE_if_process_exited_abnormally() throws Exception { + File homeDir = temp.newFolder(); + File confDir = temp.newFolder(); + when(esInstallation.getHomeDirectory()).thenReturn(homeDir); + when(esInstallation.getConfDirectory()).thenReturn(confDir); + + EsKeyStoreCli underTest = EsKeyStoreCli.getInstance(esInstallation); + underTest.store("test.property1", "value1"); + + assertThatThrownBy(() -> underTest.executeWith(EsKeyStoreCliTest::mockFailureLaunch)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Elasticsearch KeyStore tool exited with code: 1"); + } + + @Test + public void fail_if_tries_to_store_null_key() throws Exception { + File homeDir = temp.newFolder(); + File confDir = temp.newFolder(); + when(esInstallation.getHomeDirectory()).thenReturn(homeDir); + when(esInstallation.getConfDirectory()).thenReturn(confDir); + + EsKeyStoreCli underTest = EsKeyStoreCli.getInstance(esInstallation); + assertThatThrownBy(() -> underTest.store(null, "value1")) + .isInstanceOf(NullPointerException.class) + .hasMessage("Property key cannot be null"); + } + + @Test + public void fail_if_tries_to_store_null_value() throws Exception { + File homeDir = temp.newFolder(); + File confDir = temp.newFolder(); + when(esInstallation.getHomeDirectory()).thenReturn(homeDir); + when(esInstallation.getConfDirectory()).thenReturn(confDir); + + EsKeyStoreCli underTest = EsKeyStoreCli.getInstance(esInstallation); + assertThatThrownBy(() -> underTest.store("key", null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("Property value cannot be null"); + } + + private static MockProcess mockLaunch(JavaCommand javaCommand) { + return new MockProcess(javaCommand); + } + + private static MockProcess mockFailureLaunch(JavaCommand javaCommand) { + return new MockProcess(javaCommand, 1); + } + + public static class ArrayContainsMatcher implements ArgumentMatcher { + private final String left; + + public ArrayContainsMatcher(String left) { + this.left = left; + } + + @Override + public boolean matches(byte[] right) { + return new String(right).startsWith(left); + } + } + + private static class MockProcess extends Process { + JavaCommand executedCommand; + Process process; + OutputStream outputStream = mock(OutputStream.class); + + public MockProcess(JavaCommand executedCommand) { + this(executedCommand, 0); + } + + public MockProcess(JavaCommand executedCommand, int exitCode) { + this.executedCommand = executedCommand; + process = mock(Process.class); + when(process.getOutputStream()).thenReturn(outputStream); + when(process.exitValue()).thenReturn(exitCode); + } + + public Process getMock() { + return process; + } + + public JavaCommand getExecutedCommand() { + return executedCommand; + } + + @Override + public OutputStream getOutputStream() { + return outputStream; + } + + @Override + public InputStream getInputStream() { + return null; + } + + @Override + public InputStream getErrorStream() { + return null; + } + + @Override + public int waitFor() throws InterruptedException { + process.waitFor(); + return 0; + } + + @Override + public boolean waitFor(long timeout, TimeUnit unit) throws InterruptedException { + process.waitFor(timeout, unit); + return true; + } + + @Override + public int exitValue() { + return process.exitValue(); + } + + @Override + public void destroy() { + + } + } + +} -- 2.39.5