aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-main/src
diff options
context:
space:
mode:
authorJacek <jacek.poreda@sonarsource.com>2021-04-15 16:23:43 +0200
committersonartech <sonartech@sonarsource.com>2021-04-23 20:03:29 +0000
commitd8109105b156ece5352c95c0b1f2095fffcb69c2 (patch)
treed2baddf4b97e5d0d3ccad9901ab405ca2270cbe2 /server/sonar-main/src
parenta44fd9b5bdf8f128d6b40337ffcc1b7404ac3763 (diff)
downloadsonarqube-d8109105b156ece5352c95c0b1f2095fffcb69c2.tar.gz
sonarqube-d8109105b156ece5352c95c0b1f2095fffcb69c2.zip
SONAR-14583 add elasticsearch-keystore CLI utility class
Diffstat (limited to 'server/sonar-main/src')
-rw-r--r--server/sonar-main/src/main/java/org/sonar/application/ProcessLauncherImpl.java72
-rw-r--r--server/sonar-main/src/main/java/org/sonar/application/command/JavaCommand.java15
-rw-r--r--server/sonar-main/src/main/java/org/sonar/application/es/EsInstallation.java56
-rw-r--r--server/sonar-main/src/main/java/org/sonar/application/es/EsKeyStoreCli.java131
-rw-r--r--server/sonar-main/src/test/java/org/sonar/application/ProcessLauncherImplTest.java138
-rw-r--r--server/sonar-main/src/test/java/org/sonar/application/es/EsKeyStoreCliTest.java205
6 files changed, 596 insertions, 21 deletions
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<T extends JvmOptions> extends AbstractCommand<JavaCommand<T>> {
// program arguments
private final Map<String, String> arguments = new LinkedHashMap<>();
+ private final List<String> parameters = new LinkedList<>();
// entry point
private String className;
private JvmOptions<T> jvmOptions;
@@ -59,6 +61,19 @@ public class JavaCommand<T extends JvmOptions> extends AbstractCommand<JavaComma
return this;
}
+ public List<String> getParameters() {
+ return parameters;
+ }
+
+ public JavaCommand<T> 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<File> 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<String> getKeyStorePassword() {
+ return Optional.ofNullable(keyStorePassword);
+ }
+
+ public Optional<String> 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<String, String> properties = new LinkedHashMap<>();
+ private final JavaCommand<EsKeyStoreJvmOptions> command;
+
+ private EsKeyStoreCli(EsInstallation esInstallation) {
+ String esHomeAbsolutePath = esInstallation.getHomeDirectory().getAbsolutePath();
+ command = new JavaCommand<EsKeyStoreJvmOptions>(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<JavaCommand<?>, 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<String, String> 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<EsKeyStoreJvmOptions> {
+
+ public EsKeyStoreJvmOptions(EsInstallation esInstallation) {
+ super(mandatoryOptions(esInstallation));
+ }
+
+ private static Map<String, String> mandatoryOptions(EsInstallation esInstallation) {
+ Map<String, String> 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 {
@@ -92,6 +91,99 @@ public class ProcessLauncherImplTest {
}
@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<JvmOptions> 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<JvmOptions> 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<JvmOptions> 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();
TestProcessBuilder processBuilder = new TestProcessBuilder();
@@ -127,7 +219,7 @@ public class ProcessLauncherImplTest {
File tempDir = temp.newFolder();
TestProcessBuilder processBuilder = new TestProcessBuilder();
ProcessLauncher underTest = new ProcessLauncherImpl(tempDir, commands, () -> processBuilder);
- JavaCommand<JvmOptions> command = new JavaCommand<>(ProcessId.WEB_SERVER, temp.newFolder());
+ JavaCommand<JvmOptions<?>> 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<byte[]> {
+ 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() {
+
+ }
+ }
+
+}