diff options
author | Simon Brandhof <simon.brandhof@sonarsource.com> | 2017-09-14 16:33:26 +0200 |
---|---|---|
committer | Simon Brandhof <simon.brandhof@sonarsource.com> | 2017-09-26 23:49:37 +0200 |
commit | 87ce833df2754dc1a23d29e56571ba826978b7bd (patch) | |
tree | 31c73987010443b47bdff75f5aeed1d05370d094 /server/sonar-main | |
parent | e3f8991bf2bb425f2829a4767a2d5fe6e3236c8c (diff) | |
download | sonarqube-87ce833df2754dc1a23d29e56571ba826978b7bd.tar.gz sonarqube-87ce833df2754dc1a23d29e56571ba826978b7bd.zip |
SONAR-9803 restrict sonar-process to classes shared by all processes only
Diffstat (limited to 'server/sonar-main')
35 files changed, 3065 insertions, 18 deletions
diff --git a/server/sonar-main/pom.xml b/server/sonar-main/pom.xml index 9aa79852803..707e0fb6016 100644 --- a/server/sonar-main/pom.xml +++ b/server/sonar-main/pom.xml @@ -90,6 +90,17 @@ <artifactId>hazelcast-client</artifactId> <scope>test</scope> </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>sonar-testing-harness</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.tngtech.java</groupId> + <artifactId>junit-dataprovider</artifactId> + <scope>test</scope> + </dependency> + </dependencies> <build> diff --git a/server/sonar-main/src/main/java/org/sonar/application/SchedulerImpl.java b/server/sonar-main/src/main/java/org/sonar/application/SchedulerImpl.java index 3268bb249c6..0f9214e20b1 100644 --- a/server/sonar-main/src/main/java/org/sonar/application/SchedulerImpl.java +++ b/server/sonar-main/src/main/java/org/sonar/application/SchedulerImpl.java @@ -43,9 +43,9 @@ import org.sonar.application.process.ProcessLifecycleListener; import org.sonar.application.process.ProcessMonitor; import org.sonar.application.process.SQProcess; import org.sonar.process.ProcessId; -import org.sonar.process.command.CommandFactory; -import org.sonar.process.command.EsCommand; -import org.sonar.process.command.JavaCommand; +import org.sonar.application.command.CommandFactory; +import org.sonar.application.command.EsCommand; +import org.sonar.application.command.JavaCommand; public class SchedulerImpl implements Scheduler, ProcessEventListener, ProcessLifecycleListener, AppStateListener { diff --git a/server/sonar-main/src/main/java/org/sonar/application/command/AbstractCommand.java b/server/sonar-main/src/main/java/org/sonar/application/command/AbstractCommand.java new file mode 100644 index 00000000000..2da97bb8f8f --- /dev/null +++ b/server/sonar-main/src/main/java/org/sonar/application/command/AbstractCommand.java @@ -0,0 +1,104 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.command; + +import java.io.File; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import javax.annotation.Nullable; +import org.sonar.process.ProcessId; +import org.sonar.process.System2; + +import static java.util.Objects.requireNonNull; + +public abstract class AbstractCommand<T extends AbstractCommand> { + // unique key among the group of commands to launch + private final ProcessId id; + // program arguments + private final Map<String, String> arguments = new LinkedHashMap<>(); + private final Map<String, String> envVariables; + private final Set<String> suppressedEnvVariables = new HashSet<>(); + private final File workDir; + + protected AbstractCommand(ProcessId id, File workDir, System2 system2) { + this.id = requireNonNull(id, "ProcessId can't be null"); + this.workDir = requireNonNull(workDir, "workDir can't be null"); + this.envVariables = new HashMap<>(system2.getenv()); + } + + public ProcessId getProcessId() { + return id; + } + + public File getWorkDir() { + return workDir; + } + + @SuppressWarnings("unchecked") + private T castThis() { + return (T) this; + } + + public Map<String, String> getArguments() { + return arguments; + } + + public T setArgument(String key, @Nullable String value) { + if (value == null) { + arguments.remove(key); + } else { + arguments.put(key, value); + } + return castThis(); + } + + public T setArguments(Properties args) { + for (Map.Entry<Object, Object> entry : args.entrySet()) { + setArgument(entry.getKey().toString(), entry.getValue() != null ? entry.getValue().toString() : null); + } + return castThis(); + } + + public Map<String, String> getEnvVariables() { + return envVariables; + } + + public Set<String> getSuppressedEnvVariables() { + return suppressedEnvVariables; + } + + public T suppressEnvVariable(String key) { + requireNonNull(key, "key can't be null"); + suppressedEnvVariables.add(key); + envVariables.remove(key); + return castThis(); + } + + public T setEnvVariable(String key, String value) { + envVariables.put( + requireNonNull(key, "key can't be null"), + requireNonNull(value, "value can't be null")); + return castThis(); + } +} diff --git a/server/sonar-main/src/main/java/org/sonar/application/command/CeJvmOptions.java b/server/sonar-main/src/main/java/org/sonar/application/command/CeJvmOptions.java new file mode 100644 index 00000000000..57e4b1b090e --- /dev/null +++ b/server/sonar-main/src/main/java/org/sonar/application/command/CeJvmOptions.java @@ -0,0 +1,38 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.command; + +import java.io.File; +import java.util.LinkedHashMap; +import java.util.Map; + +public class CeJvmOptions extends JvmOptions<CeJvmOptions> { + public CeJvmOptions(File tmpDir) { + super(mandatoryOptions(tmpDir)); + } + + private static Map<String, String> mandatoryOptions(File tmpDir) { + Map<String, String> res = new LinkedHashMap<>(3); + res.put("-Djava.awt.headless=", "true"); + res.put("-Dfile.encoding=", "UTF-8"); + res.put("-Djava.io.tmpdir=", tmpDir.getAbsolutePath()); + return res; + } +} diff --git a/server/sonar-main/src/main/java/org/sonar/application/command/CommandFactory.java b/server/sonar-main/src/main/java/org/sonar/application/command/CommandFactory.java new file mode 100644 index 00000000000..e55f364be11 --- /dev/null +++ b/server/sonar-main/src/main/java/org/sonar/application/command/CommandFactory.java @@ -0,0 +1,30 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.command; + +public interface CommandFactory { + + EsCommand createEsCommand(); + + JavaCommand createWebCommand(boolean leader); + + JavaCommand createCeCommand(); + +} diff --git a/server/sonar-main/src/main/java/org/sonar/application/command/CommandFactoryImpl.java b/server/sonar-main/src/main/java/org/sonar/application/command/CommandFactoryImpl.java new file mode 100644 index 00000000000..0df586aa7f0 --- /dev/null +++ b/server/sonar-main/src/main/java/org/sonar/application/command/CommandFactoryImpl.java @@ -0,0 +1,166 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.command; + +import java.io.File; +import java.util.Map; +import java.util.Optional; +import org.slf4j.LoggerFactory; +import org.sonar.process.ProcessId; +import org.sonar.process.ProcessProperties; +import org.sonar.process.Props; +import org.sonar.process.System2; +import org.sonar.application.es.EsFileSystem; +import org.sonar.application.es.EsLogging; +import org.sonar.application.es.EsSettings; +import org.sonar.application.es.EsYmlSettings; + +import static org.sonar.process.ProcessProperties.HTTPS_PROXY_HOST; +import static org.sonar.process.ProcessProperties.HTTPS_PROXY_PORT; +import static org.sonar.process.ProcessProperties.HTTP_PROXY_HOST; +import static org.sonar.process.ProcessProperties.HTTP_PROXY_PORT; + +public class CommandFactoryImpl implements CommandFactory { + private static final String ENV_VAR_JAVA_TOOL_OPTIONS = "JAVA_TOOL_OPTIONS"; + /** + * Properties about proxy that must be set as system properties + */ + private static final String[] PROXY_PROPERTY_KEYS = new String[] { + HTTP_PROXY_HOST, + HTTP_PROXY_PORT, + "http.nonProxyHosts", + HTTPS_PROXY_HOST, + HTTPS_PROXY_PORT, + "http.auth.ntlm.domain", + "socksProxyHost", + "socksProxyPort"}; + + private final Props props; + private final File tempDir; + + public CommandFactoryImpl(Props props, File tempDir, System2 system2) { + this.props = props; + this.tempDir = tempDir; + String javaToolOptions = system2.getenv(ENV_VAR_JAVA_TOOL_OPTIONS); + if (javaToolOptions != null && !javaToolOptions.trim().isEmpty()) { + LoggerFactory.getLogger(CommandFactoryImpl.class) + .warn("JAVA_TOOL_OPTIONS is defined but will be ignored. " + + "Use properties sonar.*.javaOpts and/or sonar.*.javaAdditionalOpts in sonar.properties to change SQ JVM processes options"); + } + } + + @Override + public EsCommand createEsCommand() { + EsFileSystem esFileSystem = new EsFileSystem(props); + if (!esFileSystem.getExecutable().exists()) { + throw new IllegalStateException("Cannot find elasticsearch binary"); + } + Map<String, String> settingsMap = new EsSettings(props, esFileSystem, System2.INSTANCE).build(); + + return new EsCommand(ProcessId.ELASTICSEARCH, esFileSystem.getHomeDirectory()) + .setFileSystem(esFileSystem) + .setLog4j2Properties(new EsLogging().createProperties(props, esFileSystem.getLogDirectory())) + .setArguments(props.rawProperties()) + .setClusterName(settingsMap.get("cluster.name")) + .setHost(settingsMap.get("network.host")) + .setPort(Integer.valueOf(settingsMap.get("transport.tcp.port"))) + .addEsOption("-Epath.conf=" + esFileSystem.getConfDirectory().getAbsolutePath()) + .setEsJvmOptions(new EsJvmOptions() + .addFromMandatoryProperty(props, ProcessProperties.SEARCH_JAVA_OPTS) + .addFromMandatoryProperty(props, ProcessProperties.SEARCH_JAVA_ADDITIONAL_OPTS)) + .setEsYmlSettings(new EsYmlSettings(settingsMap)) + .setEnvVariable("ES_JVM_OPTIONS", esFileSystem.getJvmOptions().getAbsolutePath()) + .setEnvVariable("JAVA_HOME", System.getProperties().getProperty("java.home")) + .suppressEnvVariable(ENV_VAR_JAVA_TOOL_OPTIONS); + } + + @Override + public JavaCommand createWebCommand(boolean leader) { + File homeDir = props.nonNullValueAsFile(ProcessProperties.PATH_HOME); + + WebJvmOptions jvmOptions = new WebJvmOptions(tempDir) + .addFromMandatoryProperty(props, ProcessProperties.WEB_JAVA_OPTS) + .addFromMandatoryProperty(props, ProcessProperties.WEB_JAVA_ADDITIONAL_OPTS); + addProxyJvmOptions(jvmOptions); + + JavaCommand<WebJvmOptions> command = new JavaCommand<WebJvmOptions>(ProcessId.WEB_SERVER, homeDir) + .setArguments(props.rawProperties()) + .setJvmOptions(jvmOptions) + // required for logback tomcat valve + .setEnvVariable(ProcessProperties.PATH_LOGS, props.nonNullValue(ProcessProperties.PATH_LOGS)) + .setArgument("sonar.cluster.web.startupLeader", Boolean.toString(leader)) + .setClassName("org.sonar.server.app.WebServer") + .addClasspath("./lib/common/*") + .addClasspath("./lib/server/*"); + String driverPath = props.value(ProcessProperties.JDBC_DRIVER_PATH); + if (driverPath != null) { + command.addClasspath(driverPath); + } + command.suppressEnvVariable(ENV_VAR_JAVA_TOOL_OPTIONS); + return command; + } + + @Override + public JavaCommand createCeCommand() { + File homeDir = props.nonNullValueAsFile(ProcessProperties.PATH_HOME); + + CeJvmOptions jvmOptions = new CeJvmOptions(tempDir) + .addFromMandatoryProperty(props, ProcessProperties.CE_JAVA_OPTS) + .addFromMandatoryProperty(props, ProcessProperties.CE_JAVA_ADDITIONAL_OPTS); + addProxyJvmOptions(jvmOptions); + + JavaCommand<CeJvmOptions> command = new JavaCommand<CeJvmOptions>(ProcessId.COMPUTE_ENGINE, homeDir) + .setArguments(props.rawProperties()) + .setJvmOptions(jvmOptions) + .setClassName("org.sonar.ce.app.CeServer") + .addClasspath("./lib/common/*") + .addClasspath("./lib/server/*") + .addClasspath("./lib/ce/*"); + String driverPath = props.value(ProcessProperties.JDBC_DRIVER_PATH); + if (driverPath != null) { + command.addClasspath(driverPath); + } + command.suppressEnvVariable(ENV_VAR_JAVA_TOOL_OPTIONS); + return command; + } + + private <T extends JvmOptions> void addProxyJvmOptions(JvmOptions<T> jvmOptions) { + for (String key : PROXY_PROPERTY_KEYS) { + getPropsValue(key).ifPresent(val -> jvmOptions.add("-D" + key + "=" + val)); + } + + // defaults of HTTPS are the same than HTTP defaults + setSystemPropertyToDefaultIfNotSet(jvmOptions, HTTPS_PROXY_HOST, HTTP_PROXY_HOST); + setSystemPropertyToDefaultIfNotSet(jvmOptions, HTTPS_PROXY_PORT, HTTP_PROXY_PORT); + } + + private void setSystemPropertyToDefaultIfNotSet(JvmOptions jvmOptions, + String httpsProperty, String httpProperty) { + Optional<String> httpValue = getPropsValue(httpProperty); + Optional<String> httpsValue = getPropsValue(httpsProperty); + if (!httpsValue.isPresent() && httpValue.isPresent()) { + jvmOptions.add("-D" + httpsProperty + "=" + httpValue.get()); + } + } + + private Optional<String> getPropsValue(String key) { + return Optional.ofNullable(props.value(key)); + } +} diff --git a/server/sonar-main/src/main/java/org/sonar/application/command/EsCommand.java b/server/sonar-main/src/main/java/org/sonar/application/command/EsCommand.java new file mode 100644 index 00000000000..d44e272132f --- /dev/null +++ b/server/sonar-main/src/main/java/org/sonar/application/command/EsCommand.java @@ -0,0 +1,118 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.command; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import org.sonar.application.es.EsFileSystem; +import org.sonar.application.es.EsYmlSettings; +import org.sonar.process.ProcessId; +import org.sonar.process.System2; + +public class EsCommand extends AbstractCommand<EsCommand> { + private EsFileSystem fileSystem; + private String clusterName; + private String host; + private int port; + private Properties log4j2Properties; + private List<String> esOptions = new ArrayList<>(); + private EsJvmOptions esJvmOptions; + private EsYmlSettings esYmlSettings; + + public EsCommand(ProcessId id, File workDir) { + super(id, workDir, System2.INSTANCE); + } + + public EsFileSystem getFileSystem() { + return fileSystem; + } + + public EsCommand setFileSystem(EsFileSystem fileSystem) { + this.fileSystem = fileSystem; + return this; + } + + public String getClusterName() { + return clusterName; + } + + public EsCommand setClusterName(String clusterName) { + this.clusterName = clusterName; + return this; + } + + public String getHost() { + return host; + } + + public EsCommand setHost(String host) { + this.host = host; + return this; + } + + public int getPort() { + return port; + } + + public EsCommand setPort(int port) { + this.port = port; + return this; + } + + public Properties getLog4j2Properties() { + return log4j2Properties; + } + + public EsCommand setLog4j2Properties(Properties log4j2Properties) { + this.log4j2Properties = log4j2Properties; + return this; + } + + public List<String> getEsOptions() { + return esOptions; + } + + public EsCommand addEsOption(String s) { + if (!s.isEmpty()) { + esOptions.add(s); + } + return this; + } + + public EsCommand setEsJvmOptions(EsJvmOptions esJvmOptions) { + this.esJvmOptions = esJvmOptions; + return this; + } + + public EsJvmOptions getEsJvmOptions() { + return esJvmOptions; + } + + public EsCommand setEsYmlSettings(EsYmlSettings esYmlSettings) { + this.esYmlSettings = esYmlSettings; + return this; + } + + public EsYmlSettings getEsYmlSettings() { + return esYmlSettings; + } +} diff --git a/server/sonar-main/src/main/java/org/sonar/application/command/EsJvmOptions.java b/server/sonar-main/src/main/java/org/sonar/application/command/EsJvmOptions.java new file mode 100644 index 00000000000..bf2dfd680b3 --- /dev/null +++ b/server/sonar-main/src/main/java/org/sonar/application/command/EsJvmOptions.java @@ -0,0 +1,71 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.command; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; + +public class EsJvmOptions extends JvmOptions<EsJvmOptions> { + private static final String ELASTICSEARCH_JVM_OPTIONS_HEADER = "# This file has been automatically generated by SonarQube during startup.\n" + + "# Please use sonar.search.javaOpts and/or sonar.search.javaAdditionalOpts in sonar.properties to specify jvm options for Elasticsearch\n" + + "\n" + + "# DO NOT EDIT THIS FILE\n" + + "\n"; + + public EsJvmOptions() { + super(mandatoryOptions()); + } + + private static Map<String, String> mandatoryOptions() { + Map<String, String> res = new LinkedHashMap<>(16); + res.put("-XX:+UseConcMarkSweepGC", ""); + res.put("-XX:CMSInitiatingOccupancyFraction=", "75"); + res.put("-XX:+UseCMSInitiatingOccupancyOnly", ""); + res.put("-XX:+AlwaysPreTouch", ""); + res.put("-server", ""); + res.put("-Xss", "1m"); + res.put("-Djava.awt.headless=", "true"); + res.put("-Dfile.encoding=", "UTF-8"); + res.put("-Djna.nosys=", "true"); + res.put("-Djdk.io.permissionsUseCanonicalPath=", "true"); + res.put("-Dio.netty.noUnsafe=", "true"); + res.put("-Dio.netty.noKeySetOptimization=", "true"); + res.put("-Dio.netty.recycler.maxCapacityPerThread=", "0"); + res.put("-Dlog4j.shutdownHookEnabled=", "false"); + res.put("-Dlog4j2.disable.jmx=", "true"); + res.put("-Dlog4j.skipJansi=", "true"); + return res; + } + + public void writeToJvmOptionFile(File file) { + String jvmOptions = getAll().stream().collect(Collectors.joining("\n")); + String jvmOptionsContent = ELASTICSEARCH_JVM_OPTIONS_HEADER + jvmOptions; + try { + Files.write(file.toPath(), jvmOptionsContent.getBytes(Charset.forName("UTF-8"))); + } catch (IOException e) { + throw new IllegalStateException("Cannot write Elasticsearch jvm options file", e); + } + } +} 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 new file mode 100644 index 00000000000..5cee5a95e33 --- /dev/null +++ b/server/sonar-main/src/main/java/org/sonar/application/command/JavaCommand.java @@ -0,0 +1,78 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.command; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import org.sonar.process.ProcessId; +import org.sonar.process.System2; + +public class JavaCommand<T extends JvmOptions> extends AbstractCommand<JavaCommand<T>> { + // entry point + private String className; + private JvmOptions<T> jvmOptions; + // relative path to JAR files + private final List<String> classpath = new ArrayList<>(); + + public JavaCommand(ProcessId id, File workDir) { + super(id, workDir, System2.INSTANCE); + } + + public JvmOptions<T> getJvmOptions() { + return jvmOptions; + } + + public JavaCommand<T> setJvmOptions(JvmOptions<T> jvmOptions) { + this.jvmOptions = jvmOptions; + + return this; + } + + public String getClassName() { + return className; + } + + public JavaCommand<T> setClassName(String className) { + this.className = className; + return this; + } + + public List<String> getClasspath() { + return classpath; + } + + public JavaCommand<T> addClasspath(String s) { + classpath.add(s); + return this; + } + + @Override + public String toString() { + return "JavaCommand{" + "workDir=" + getWorkDir() + + ", jvmOptions=" + jvmOptions + + ", className='" + className + '\'' + + ", classpath=" + classpath + + ", arguments=" + getArguments() + + ", envVariables=" + getEnvVariables() + + ", suppressedEnvVariables=" + getSuppressedEnvVariables() + + '}'; + } +} diff --git a/server/sonar-main/src/main/java/org/sonar/application/command/JvmOptions.java b/server/sonar-main/src/main/java/org/sonar/application/command/JvmOptions.java new file mode 100644 index 00000000000..3ef1b3bfd20 --- /dev/null +++ b/server/sonar-main/src/main/java/org/sonar/application/command/JvmOptions.java @@ -0,0 +1,182 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.command; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.sonar.process.MessageException; +import org.sonar.process.Props; + +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.joining; + +public class JvmOptions<T extends JvmOptions> { + private static final String JVM_OPTION_NOT_NULL_ERROR_MESSAGE = "a JVM option can't be null"; + + private final HashMap<String, String> mandatoryOptions = new HashMap<>(); + private final LinkedHashSet<String> options = new LinkedHashSet<>(); + + public JvmOptions() { + this(Collections.emptyMap()); + } + + public JvmOptions(Map<String, String> mandatoryJvmOptions) { + requireNonNull(mandatoryJvmOptions, JVM_OPTION_NOT_NULL_ERROR_MESSAGE) + .entrySet() + .stream() + .filter(e -> { + requireNonNull(e.getKey(), "JVM option prefix can't be null"); + if (e.getKey().trim().isEmpty()) { + throw new IllegalArgumentException("JVM option prefix can't be empty"); + } + requireNonNull(e.getValue(), "JVM option value can't be null"); + return true; + }).forEach(e -> { + String key = e.getKey().trim(); + String value = e.getValue().trim(); + mandatoryOptions.put(key, value); + add(key + value); + }); + } + + public T addFromMandatoryProperty(Props props, String propertyName) { + String value = props.nonNullValue(propertyName); + if (!value.isEmpty()) { + List<String> jvmOptions = Arrays.stream(value.split(" (?=-)")).map(String::trim).collect(Collectors.toList()); + checkOptionFormat(propertyName, jvmOptions); + checkMandatoryOptionOverwrite(propertyName, jvmOptions); + options.addAll(jvmOptions); + } + + return castThis(); + } + + private static void checkOptionFormat(String propertyName, List<String> jvmOptionsFromProperty) { + List<String> invalidOptions = jvmOptionsFromProperty.stream() + .filter(JvmOptions::isInvalidOption) + .collect(Collectors.toList()); + if (!invalidOptions.isEmpty()) { + throw new MessageException(format( + "a JVM option can't be empty and must start with '-'. The following JVM options defined by property '%s' are invalid: %s", + propertyName, + invalidOptions.stream() + .collect(joining(", ")))); + } + } + + private void checkMandatoryOptionOverwrite(String propertyName, List<String> jvmOptionsFromProperty) { + List<Match> matches = jvmOptionsFromProperty.stream() + .map(jvmOption -> new Match(jvmOption, mandatoryOptionFor(jvmOption))) + .filter(match -> match.getMandatoryOption() != null) + .collect(Collectors.toList()); + if (!matches.isEmpty()) { + throw new MessageException(format( + "a JVM option can't overwrite mandatory JVM options. The following JVM options defined by property '%s' are invalid: %s", + propertyName, + matches.stream() + .map(m -> m.getOption() + " overwrites " + m.mandatoryOption.getKey() + m.mandatoryOption.getValue()) + .collect(joining(", ")))); + } + } + + /** + * Add an option. + * Argument is trimmed before being added. + * + * @throws IllegalArgumentException if argument is empty or does not start with {@code -}. + */ + public T add(String str) { + requireNonNull(str, JVM_OPTION_NOT_NULL_ERROR_MESSAGE); + String value = str.trim(); + if (isInvalidOption(value)) { + throw new IllegalArgumentException("a JVM option can't be empty and must start with '-'"); + } + checkMandatoryOptionOverwrite(value); + options.add(value); + + return castThis(); + } + + private void checkMandatoryOptionOverwrite(String value) { + Map.Entry<String, String> overriddenMandatoryOption = mandatoryOptionFor(value); + if (overriddenMandatoryOption != null) { + throw new MessageException(String.format( + "a JVM option can't overwrite mandatory JVM options. %s overwrites %s", + value, + overriddenMandatoryOption.getKey() + overriddenMandatoryOption.getValue())); + } + } + + @CheckForNull + private Map.Entry<String, String> mandatoryOptionFor(String jvmOption) { + return mandatoryOptions.entrySet().stream() + .filter(s -> jvmOption.startsWith(s.getKey()) && !jvmOption.equals(s.getKey() + s.getValue())) + .findFirst() + .orElse(null); + } + + private static boolean isInvalidOption(String value) { + return value.isEmpty() || !value.startsWith("-"); + } + + @SuppressWarnings("unchecked") + private T castThis() { + return (T) this; + } + + public List<String> getAll() { + return new ArrayList<>(options); + } + + @Override + public String toString() { + return options.toString(); + } + + private static final class Match { + private final String option; + + private final Map.Entry<String, String> mandatoryOption; + + private Match(String option, @Nullable Map.Entry<String, String> mandatoryOption) { + this.option = option; + this.mandatoryOption = mandatoryOption; + } + + String getOption() { + return option; + } + + @CheckForNull + Map.Entry<String, String> getMandatoryOption() { + return mandatoryOption; + } + + } +} diff --git a/server/sonar-main/src/main/java/org/sonar/application/command/WebJvmOptions.java b/server/sonar-main/src/main/java/org/sonar/application/command/WebJvmOptions.java new file mode 100644 index 00000000000..9a066a736f8 --- /dev/null +++ b/server/sonar-main/src/main/java/org/sonar/application/command/WebJvmOptions.java @@ -0,0 +1,38 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.command; + +import java.io.File; +import java.util.LinkedHashMap; +import java.util.Map; + +public class WebJvmOptions extends JvmOptions<WebJvmOptions> { + public WebJvmOptions(File tmpDir) { + super(mandatoryOptions(tmpDir)); + } + + private static Map<String, String> mandatoryOptions(File tmpDir) { + Map<String, String> res = new LinkedHashMap<>(3); + res.put("-Djava.awt.headless=", "true"); + res.put("-Dfile.encoding=", "UTF-8"); + res.put("-Djava.io.tmpdir=", tmpDir.getAbsolutePath()); + return res; + } +} diff --git a/server/sonar-main/src/main/java/org/sonar/application/command/package-info.java b/server/sonar-main/src/main/java/org/sonar/application/command/package-info.java new file mode 100644 index 00000000000..a083f0ab81a --- /dev/null +++ b/server/sonar-main/src/main/java/org/sonar/application/command/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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. + */ +@ParametersAreNonnullByDefault +package org.sonar.application.command; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-main/src/main/java/org/sonar/application/es/EsFileSystem.java b/server/sonar-main/src/main/java/org/sonar/application/es/EsFileSystem.java new file mode 100644 index 00000000000..73fc79edbe4 --- /dev/null +++ b/server/sonar-main/src/main/java/org/sonar/application/es/EsFileSystem.java @@ -0,0 +1,105 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 org.apache.commons.lang.StringUtils; +import org.sonar.process.ProcessProperties; +import org.sonar.process.Props; + +/** + * Holds {@link File} to the various directories of ElasticSearch distribution embedded in SonarQube and provides + * {@link File} objects to the various files of it SonarQube cares about. + * + * <p> + * This class does not ensure files nor directories actually exist. + * </p> + */ +public class EsFileSystem { + private final File homeDirectory; + private final File dataDirectory; + private final File confDirectory; + private final File logDirectory; + + public EsFileSystem(Props props) { + File sqHomeDir = props.nonNullValueAsFile(ProcessProperties.PATH_HOME); + + this.homeDirectory = new File(sqHomeDir, "elasticsearch"); + this.dataDirectory = buildDataDir(props, sqHomeDir); + this.confDirectory = buildConfDir(props); + this.logDirectory = buildLogPath(props); + } + + private static File buildDataDir(Props props, File sqHomeDir) { + String dataPath = props.value(ProcessProperties.PATH_DATA); + if (StringUtils.isNotEmpty(dataPath)) { + return new File(dataPath, "es"); + } + return new File(sqHomeDir, "data/es"); + } + + private static File buildLogPath(Props props) { + return props.nonNullValueAsFile(ProcessProperties.PATH_LOGS); + } + + private static File buildConfDir(Props props) { + File tempPath = props.nonNullValueAsFile(ProcessProperties.PATH_TEMP); + return new File(new File(tempPath, "conf"), "es"); + } + + public File getHomeDirectory() { + return homeDirectory; + } + + public File getDataDirectory() { + return dataDirectory; + } + + public File getConfDirectory() { + return confDirectory; + } + + public File getLogDirectory() { + return logDirectory; + } + + public File getExecutable() { + return new File(homeDirectory, "bin/" + getExecutableName()); + } + + private static String getExecutableName() { + if (System.getProperty("os.name").startsWith("Windows")) { + return "elasticsearch.bat"; + } + return "elasticsearch"; + } + + public File getLog4j2Properties() { + return new File(confDirectory, "log4j2.properties"); + } + + public File getElasticsearchYml() { + return new File(confDirectory, "elasticsearch.yml"); + } + + public File getJvmOptions() { + return new File(confDirectory, "jvm.options"); + } +} diff --git a/server/sonar-main/src/main/java/org/sonar/application/es/EsLogging.java b/server/sonar-main/src/main/java/org/sonar/application/es/EsLogging.java new file mode 100644 index 00000000000..90fb1d74c2c --- /dev/null +++ b/server/sonar-main/src/main/java/org/sonar/application/es/EsLogging.java @@ -0,0 +1,50 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 ch.qos.logback.classic.Level; +import java.io.File; +import java.util.Properties; +import org.sonar.process.ProcessId; +import org.sonar.process.Props; +import org.sonar.process.logging.Log4JPropertiesBuilder; +import org.sonar.process.logging.LogLevelConfig; +import org.sonar.process.logging.RootLoggerConfig; + +import static org.sonar.process.logging.RootLoggerConfig.newRootLoggerConfigBuilder; + +public class EsLogging { + + public Properties createProperties(Props props, File logDir) { + Log4JPropertiesBuilder log4JPropertiesBuilder = new Log4JPropertiesBuilder(props); + RootLoggerConfig config = newRootLoggerConfigBuilder().setProcessId(ProcessId.ELASTICSEARCH).build(); + String logPattern = log4JPropertiesBuilder.buildLogPattern(config); + + log4JPropertiesBuilder.internalLogLevel(Level.ERROR); + log4JPropertiesBuilder.configureGlobalFileLog(config, logDir, logPattern); + log4JPropertiesBuilder.apply( + LogLevelConfig.newBuilder(log4JPropertiesBuilder.getRootLoggerName()) + .rootLevelFor(ProcessId.ELASTICSEARCH) + .build()); + + return log4JPropertiesBuilder.get(); + } + +} diff --git a/server/sonar-main/src/main/java/org/sonar/application/es/EsSettings.java b/server/sonar-main/src/main/java/org/sonar/application/es/EsSettings.java new file mode 100644 index 00000000000..eb69acdbd61 --- /dev/null +++ b/server/sonar-main/src/main/java/org/sonar/application/es/EsSettings.java @@ -0,0 +1,148 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.net.InetAddress; +import java.net.UnknownHostException; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.process.ProcessProperties; +import org.sonar.process.Props; +import org.sonar.process.System2; + +import static java.lang.String.valueOf; +import static org.sonar.cluster.ClusterProperties.CLUSTER_ENABLED; +import static org.sonar.cluster.ClusterProperties.CLUSTER_NAME; +import static org.sonar.cluster.ClusterProperties.CLUSTER_NODE_NAME; +import static org.sonar.cluster.ClusterProperties.CLUSTER_SEARCH_HOSTS; + +public class EsSettings { + + private static final Logger LOGGER = LoggerFactory.getLogger(EsSettings.class); + private static final String STANDALONE_NODE_NAME = "sonarqube"; + + private final Props props; + private final EsFileSystem fileSystem; + + private final boolean clusterEnabled; + private final String clusterName; + private final String nodeName; + + public EsSettings(Props props, EsFileSystem fileSystem, System2 system2) { + this.props = props; + this.fileSystem = fileSystem; + + this.clusterName = props.nonNullValue(CLUSTER_NAME); + this.clusterEnabled = props.valueAsBoolean(CLUSTER_ENABLED); + if (this.clusterEnabled) { + this.nodeName = props.value(CLUSTER_NODE_NAME, "sonarqube-" + UUID.randomUUID().toString()); + } else { + this.nodeName = STANDALONE_NODE_NAME; + } + String esJvmOptions = system2.getenv("ES_JVM_OPTIONS"); + if (esJvmOptions != null && !esJvmOptions.trim().isEmpty()) { + LOGGER.warn("ES_JVM_OPTIONS is defined but will be ignored. " + + "Use sonar.search.javaOpts and/or sonar.search.javaAdditionalOpts in sonar.properties to specify jvm options for Elasticsearch"); + } + } + + public Map<String, String> build() { + Map<String, String> builder = new HashMap<>(); + configureFileSystem(builder); + configureNetwork(builder); + configureCluster(builder); + configureAction(builder); + return builder; + } + + private void configureFileSystem(Map<String, String> builder) { + builder.put("path.data", fileSystem.getDataDirectory().getAbsolutePath()); + builder.put("path.conf", fileSystem.getConfDirectory().getAbsolutePath()); + builder.put("path.logs", fileSystem.getLogDirectory().getAbsolutePath()); + } + + private void configureNetwork(Map<String, String> builder) { + InetAddress host = readHost(); + int port = Integer.parseInt(props.nonNullValue(ProcessProperties.SEARCH_PORT)); + LOGGER.info("Elasticsearch listening on {}:{}", host, port); + + builder.put("transport.tcp.port", valueOf(port)); + builder.put("transport.host", valueOf(host.getHostAddress())); + builder.put("network.host", valueOf(host.getHostAddress())); + + // Elasticsearch sets the default value of TCP reuse address to true only on non-MSWindows machines, but why ? + builder.put("network.tcp.reuse_address", valueOf(true)); + + int httpPort = props.valueAsInt(ProcessProperties.SEARCH_HTTP_PORT, -1); + if (httpPort < 0) { + // standard configuration + builder.put("http.enabled", valueOf(false)); + } else { + LOGGER.warn("Elasticsearch HTTP connector is enabled on port {}. MUST NOT BE USED FOR PRODUCTION", httpPort); + // see https://github.com/lmenezes/elasticsearch-kopf/issues/195 + builder.put("http.cors.enabled", valueOf(true)); + builder.put("http.cors.allow-origin", "*"); + builder.put("http.enabled", valueOf(true)); + builder.put("http.host", host.getHostAddress()); + builder.put("http.port", valueOf(httpPort)); + } + } + + private InetAddress readHost() { + String hostProperty = props.nonNullValue(ProcessProperties.SEARCH_HOST); + try { + return InetAddress.getByName(hostProperty); + } catch (UnknownHostException e) { + throw new IllegalStateException("Can not resolve host [" + hostProperty + "]. Please check network settings and property " + ProcessProperties.SEARCH_HOST, e); + } + } + + private void configureCluster(Map<String, String> builder) { + // Default value in a standalone mode, not overridable + + int minimumMasterNodes = 1; + String initialStateTimeOut = "30s"; + + if (clusterEnabled) { + minimumMasterNodes = props.valueAsInt(ProcessProperties.SEARCH_MINIMUM_MASTER_NODES, 2); + initialStateTimeOut = props.value(ProcessProperties.SEARCH_INITIAL_STATE_TIMEOUT, "120s"); + + String hosts = props.value(CLUSTER_SEARCH_HOSTS, ""); + LOGGER.info("Elasticsearch cluster enabled. Connect to hosts [{}]", hosts); + builder.put("discovery.zen.ping.unicast.hosts", hosts); + } + + builder.put("discovery.zen.minimum_master_nodes", valueOf(minimumMasterNodes)); + builder.put("discovery.initial_state_timeout", initialStateTimeOut); + builder.put("cluster.name", clusterName); + builder.put("cluster.routing.allocation.awareness.attributes", "rack_id"); + builder.put("node.attr.rack_id", nodeName); + builder.put("node.name", nodeName); + builder.put("node.data", valueOf(true)); + builder.put("node.master", valueOf(true)); + } + + private static void configureAction(Map<String, String> builder) { + builder.put("action.auto_create_index", String.valueOf(false)); + } +} diff --git a/server/sonar-main/src/main/java/org/sonar/application/es/EsYmlSettings.java b/server/sonar-main/src/main/java/org/sonar/application/es/EsYmlSettings.java new file mode 100644 index 00000000000..3ba92d55cbc --- /dev/null +++ b/server/sonar-main/src/main/java/org/sonar/application/es/EsYmlSettings.java @@ -0,0 +1,56 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.util.Map; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; + +import static org.yaml.snakeyaml.DumperOptions.FlowStyle.BLOCK; + +public class EsYmlSettings { + private static final String ELASTICSEARCH_YML_OPTIONS_HEADER = "# This file has been automatically generated by SonarQube during startup.\n" + + "\n" + + "# DO NOT EDIT THIS FILE\n" + + "\n"; + + private final Map<String, String> elasticsearchSettings; + + public EsYmlSettings(Map<String, String> elasticsearchSettings) { + this.elasticsearchSettings = elasticsearchSettings; + } + + public void writeToYmlSettingsFile(File file) { + DumperOptions dumperOptions = new DumperOptions(); + dumperOptions.setPrettyFlow(true); + dumperOptions.setDefaultFlowStyle(BLOCK); + Yaml yaml = new Yaml(dumperOptions); + String output = ELASTICSEARCH_YML_OPTIONS_HEADER + yaml.dump(elasticsearchSettings); + try { + Files.write(file.toPath(), output.getBytes(Charset.forName("UTF-8"))); + } catch (IOException e) { + throw new IllegalStateException("Cannot write Elasticsearch yml settings file", e); + } + } +} diff --git a/server/sonar-main/src/main/java/org/sonar/application/es/package-info.java b/server/sonar-main/src/main/java/org/sonar/application/es/package-info.java new file mode 100644 index 00000000000..8f296059727 --- /dev/null +++ b/server/sonar-main/src/main/java/org/sonar/application/es/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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. + */ +@ParametersAreNonnullByDefault +package org.sonar.application.es; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-main/src/main/java/org/sonar/application/process/EsProcessMonitor.java b/server/sonar-main/src/main/java/org/sonar/application/process/EsProcessMonitor.java index e4730c7303d..fd611188f95 100644 --- a/server/sonar-main/src/main/java/org/sonar/application/process/EsProcessMonitor.java +++ b/server/sonar-main/src/main/java/org/sonar/application/process/EsProcessMonitor.java @@ -38,7 +38,7 @@ import org.elasticsearch.discovery.MasterNotDiscoveredException; import org.elasticsearch.transport.Netty4Plugin; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.sonar.process.command.EsCommand; +import org.sonar.application.command.EsCommand; import static java.util.Collections.singletonList; import static java.util.Collections.unmodifiableList; diff --git a/server/sonar-main/src/main/java/org/sonar/application/process/ProcessLauncher.java b/server/sonar-main/src/main/java/org/sonar/application/process/ProcessLauncher.java index c39f91bc8fa..c0ca8ec7825 100644 --- a/server/sonar-main/src/main/java/org/sonar/application/process/ProcessLauncher.java +++ b/server/sonar-main/src/main/java/org/sonar/application/process/ProcessLauncher.java @@ -20,8 +20,8 @@ package org.sonar.application.process; import java.io.Closeable; -import org.sonar.process.command.EsCommand; -import org.sonar.process.command.JavaCommand; +import org.sonar.application.command.EsCommand; +import org.sonar.application.command.JavaCommand; public interface ProcessLauncher extends Closeable { diff --git a/server/sonar-main/src/main/java/org/sonar/application/process/ProcessLauncherImpl.java b/server/sonar-main/src/main/java/org/sonar/application/process/ProcessLauncherImpl.java index b0b968f05eb..1a8daf01ae3 100644 --- a/server/sonar-main/src/main/java/org/sonar/application/process/ProcessLauncherImpl.java +++ b/server/sonar-main/src/main/java/org/sonar/application/process/ProcessLauncherImpl.java @@ -31,12 +31,12 @@ import java.util.Properties; import java.util.function.Supplier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.sonar.application.command.AbstractCommand; +import org.sonar.application.command.EsCommand; +import org.sonar.application.command.JavaCommand; +import org.sonar.application.command.JvmOptions; +import org.sonar.application.es.EsFileSystem; import org.sonar.process.ProcessId; -import org.sonar.process.command.AbstractCommand; -import org.sonar.process.command.EsCommand; -import org.sonar.process.command.JavaCommand; -import org.sonar.process.es.EsFileSystem; -import org.sonar.process.jmvoptions.JvmOptions; import org.sonar.process.sharedmemoryfile.AllProcessesCommands; import org.sonar.process.sharedmemoryfile.ProcessCommands; diff --git a/server/sonar-main/src/test/java/org/sonar/application/SchedulerImplTest.java b/server/sonar-main/src/test/java/org/sonar/application/SchedulerImplTest.java index f419f4d0e17..a2ada4bb3f8 100644 --- a/server/sonar-main/src/test/java/org/sonar/application/SchedulerImplTest.java +++ b/server/sonar-main/src/test/java/org/sonar/application/SchedulerImplTest.java @@ -43,10 +43,10 @@ import org.sonar.application.process.ProcessLauncher; import org.sonar.application.process.ProcessMonitor; import org.sonar.cluster.localclient.HazelcastClient; import org.sonar.process.ProcessId; -import org.sonar.process.command.AbstractCommand; -import org.sonar.process.command.CommandFactory; -import org.sonar.process.command.EsCommand; -import org.sonar.process.command.JavaCommand; +import org.sonar.application.command.AbstractCommand; +import org.sonar.application.command.CommandFactory; +import org.sonar.application.command.EsCommand; +import org.sonar.application.command.JavaCommand; import static java.util.Collections.synchronizedList; import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; diff --git a/server/sonar-main/src/test/java/org/sonar/application/command/AbstractCommandTest.java b/server/sonar-main/src/test/java/org/sonar/application/command/AbstractCommandTest.java new file mode 100644 index 00000000000..c5ff76bc4dc --- /dev/null +++ b/server/sonar-main/src/test/java/org/sonar/application/command/AbstractCommandTest.java @@ -0,0 +1,148 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.command; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.Random; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; +import org.mockito.Mockito; +import org.sonar.process.ProcessId; +import org.sonar.process.System2; + +import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +public class AbstractCommandTest { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void constructor_throws_NPE_of_ProcessId_is_null() throws IOException { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("ProcessId can't be null"); + + new AbstractCommand<AbstractCommand>(null, temp.newFolder(), System2.INSTANCE) { + + }; + } + + @Test + public void constructor_throws_NPE_of_workDir_is_null() throws IOException { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("workDir can't be null"); + + new AbstractCommand<AbstractCommand>(ProcessId.WEB_SERVER, null, System2.INSTANCE) { + + }; + } + + @Test + public void test_command_with_complete_information() throws Exception { + File workDir = temp.newFolder(); + AbstractCommand command = new AbstractCommand(ProcessId.ELASTICSEARCH, workDir, System2.INSTANCE) { + + }; + + command.setArgument("first_arg", "val1"); + Properties args = new Properties(); + args.setProperty("second_arg", "val2"); + command.setArguments(args); + + command.setEnvVariable("JAVA_COMMAND_TEST", "1000"); + + assertThat(command.toString()).isNotNull(); + assertThat(command.getWorkDir()).isSameAs(workDir); + + // copy current env variables + assertThat(command.getEnvVariables().get("JAVA_COMMAND_TEST")).isEqualTo("1000"); + assertThat(command.getEnvVariables().size()).isEqualTo(System.getenv().size() + 1); + } + + @Test + public void setEnvVariable_fails_with_NPE_if_key_is_null() throws IOException { + File workDir = temp.newFolder(); + AbstractCommand underTest = new AbstractCommand(ProcessId.ELASTICSEARCH, workDir, System2.INSTANCE) { + + }; + + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("key can't be null"); + + underTest.setEnvVariable(null, randomAlphanumeric(30)); + } + + @Test + public void setEnvVariable_fails_with_NPE_if_value_is_null() throws IOException { + File workDir = temp.newFolder(); + AbstractCommand underTest = new AbstractCommand(ProcessId.ELASTICSEARCH, workDir, System2.INSTANCE) { + + }; + + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("value can't be null"); + + underTest.setEnvVariable(randomAlphanumeric(30), null); + } + + @Test + public void constructor_puts_System_getEnv_into_map_of_env_variables() throws IOException { + File workDir = temp.newFolder(); + System2 system2 = Mockito.mock(System2.class); + Map<String, String> env = IntStream.range(0, 1 + new Random().nextInt(99)).mapToObj(String::valueOf).collect(Collectors.toMap(i -> "key" + i, j -> "value" + j)); + when(system2.getenv()).thenReturn(env); + AbstractCommand underTest = new AbstractCommand(ProcessId.ELASTICSEARCH, workDir, system2) { + + }; + + assertThat(underTest.getEnvVariables()).isEqualTo(env); + } + + @Test + public void suppressEnvVariable_remove_existing_env_variable_and_add_variable_to_set_of_suppressed_variables() throws IOException { + File workDir = temp.newFolder(); + System2 system2 = Mockito.mock(System2.class); + Map<String, String> env = new HashMap<>(); + String key1 = randomAlphanumeric(3); + env.put(key1, randomAlphanumeric(9)); + when(system2.getenv()).thenReturn(env); + AbstractCommand underTest = new AbstractCommand(ProcessId.ELASTICSEARCH, workDir, system2) { + + }; + + underTest.suppressEnvVariable(key1); + + assertThat(underTest.getEnvVariables()).doesNotContainKey(key1); + assertThat(underTest.getSuppressedEnvVariables()).containsOnly(key1); + } + +} diff --git a/server/sonar-main/src/test/java/org/sonar/application/command/CeJvmOptionsTest.java b/server/sonar-main/src/test/java/org/sonar/application/command/CeJvmOptionsTest.java new file mode 100644 index 00000000000..4084336249c --- /dev/null +++ b/server/sonar-main/src/test/java/org/sonar/application/command/CeJvmOptionsTest.java @@ -0,0 +1,42 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.command; + +import java.io.File; +import java.io.IOException; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CeJvmOptionsTest { + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void constructor_sets_mandatory_JVM_options() throws IOException { + File tmpDir = temporaryFolder.newFolder(); + CeJvmOptions underTest = new CeJvmOptions(tmpDir); + + assertThat(underTest.getAll()).containsExactly( + "-Djava.awt.headless=true", "-Dfile.encoding=UTF-8", "-Djava.io.tmpdir=" + tmpDir.getAbsolutePath()); + } +} diff --git a/server/sonar-main/src/test/java/org/sonar/application/command/CommandFactoryImplTest.java b/server/sonar-main/src/test/java/org/sonar/application/command/CommandFactoryImplTest.java new file mode 100644 index 00000000000..ceb13a08dac --- /dev/null +++ b/server/sonar-main/src/test/java/org/sonar/application/command/CommandFactoryImplTest.java @@ -0,0 +1,239 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.command; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import java.io.File; +import java.io.IOException; +import java.util.Properties; +import org.apache.commons.io.FileUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; +import org.mockito.Mockito; +import org.sonar.process.ProcessId; +import org.sonar.process.ProcessProperties; +import org.sonar.process.Props; +import org.sonar.process.System2; +import org.sonar.application.logging.ListAppender; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.when; + +public class CommandFactoryImplTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + private File homeDir; + private File tempDir; + private File logsDir; + private ListAppender listAppender; + + @Before + public void setUp() throws Exception { + homeDir = temp.newFolder(); + tempDir = temp.newFolder(); + logsDir = temp.newFolder(); + } + + @After + public void tearDown() throws Exception { + if (listAppender != null) { + ListAppender.detachMemoryAppenderToLoggerOf(CommandFactoryImpl.class, listAppender); + } + } + + @Test + public void constructor_logs_no_warning_if_env_variable_JAVA_TOOL_OPTIONS_is_not_set() { + System2 system2 = Mockito.mock(System2.class); + when(system2.getenv(anyString())).thenReturn(null); + attachMemoryAppenderToLoggerOf(CommandFactoryImpl.class); + + new CommandFactoryImpl(new Props(new Properties()), tempDir, system2); + + assertThat(listAppender.getLogs()).isEmpty(); + } + + @Test + public void constructor_logs_warning_if_env_variable_JAVA_TOOL_OPTIONS_is_set() { + System2 system2 = Mockito.mock(System2.class); + when(system2.getenv("JAVA_TOOL_OPTIONS")).thenReturn("sds"); + attachMemoryAppenderToLoggerOf(CommandFactoryImpl.class); + + new CommandFactoryImpl(new Props(new Properties()), tempDir, system2); + + assertThat(listAppender.getLogs()) + .extracting(ILoggingEvent::getMessage) + .containsOnly( + "JAVA_TOOL_OPTIONS is defined but will be ignored. " + + "Use properties sonar.*.javaOpts and/or sonar.*.javaAdditionalOpts in sonar.properties to change SQ JVM processes options"); + } + + @Test + public void createEsCommand_throws_ISE_if_es_binary_is_not_found() throws Exception { + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Cannot find elasticsearch binary"); + + newFactory(new Properties()).createEsCommand(); + } + + @Test + public void createEsCommand_returns_command_for_default_settings() throws Exception { + prepareEsFileSystem(); + + Properties props = new Properties(); + props.setProperty("sonar.search.host", "localhost"); + + EsCommand esCommand = newFactory(props).createEsCommand(); + + assertThat(esCommand.getClusterName()).isEqualTo("sonarqube"); + assertThat(esCommand.getHost()).isNotEmpty(); + assertThat(esCommand.getPort()).isEqualTo(9001); + assertThat(esCommand.getEsJvmOptions().getAll()) + // enforced values + .contains("-XX:+UseConcMarkSweepGC", "-server", "-Dfile.encoding=UTF-8") + // default settings + .contains("-Xms512m", "-Xmx512m", "-XX:+HeapDumpOnOutOfMemoryError"); + File esConfDir = new File(tempDir, "conf/es"); + assertThat(esCommand.getEsOptions()).containsOnly("-Epath.conf=" + esConfDir.getAbsolutePath()); + assertThat(esCommand.getEnvVariables()) + .contains(entry("ES_JVM_OPTIONS", new File(esConfDir, "jvm.options").getAbsolutePath())) + .containsKey("JAVA_HOME"); + assertThat(esCommand.getEsYmlSettings()).isNotNull(); + + assertThat(esCommand.getLog4j2Properties()) + .contains(entry("appender.file_es.fileName", new File(logsDir, "es.log").getAbsolutePath())); + + assertThat(esCommand.getSuppressedEnvVariables()).containsOnly("JAVA_TOOL_OPTIONS"); + } + + @Test + public void createEsCommand_returns_command_for_overridden_settings() throws Exception { + prepareEsFileSystem(); + + Properties props = new Properties(); + props.setProperty("sonar.search.host", "localhost"); + props.setProperty("sonar.cluster.name", "foo"); + props.setProperty("sonar.search.port", "1234"); + props.setProperty("sonar.search.javaOpts", "-Xms10G -Xmx10G"); + + EsCommand command = newFactory(props).createEsCommand(); + + assertThat(command.getClusterName()).isEqualTo("foo"); + assertThat(command.getPort()).isEqualTo(1234); + assertThat(command.getEsJvmOptions().getAll()) + // enforced values + .contains("-XX:+UseConcMarkSweepGC", "-server", "-Dfile.encoding=UTF-8") + // user settings + .contains("-Xms10G", "-Xmx10G") + // default values disabled + .doesNotContain("-XX:+HeapDumpOnOutOfMemoryError"); + } + + @Test + public void createWebCommand_returns_command_for_default_settings() throws Exception { + JavaCommand command = newFactory(new Properties()).createWebCommand(true); + + assertThat(command.getClassName()).isEqualTo("org.sonar.server.app.WebServer"); + assertThat(command.getWorkDir().getAbsolutePath()).isEqualTo(homeDir.getAbsolutePath()); + assertThat(command.getClasspath()) + .containsExactlyInAnyOrder("./lib/common/*", "./lib/server/*"); + assertThat(command.getJvmOptions().getAll()) + // enforced values + .contains("-Djava.awt.headless=true", "-Dfile.encoding=UTF-8") + // default settings + .contains("-Djava.io.tmpdir=" + tempDir.getAbsolutePath(), "-Dfile.encoding=UTF-8") + .contains("-Xmx512m", "-Xms128m", "-XX:+HeapDumpOnOutOfMemoryError"); + assertThat(command.getProcessId()).isEqualTo(ProcessId.WEB_SERVER); + assertThat(command.getEnvVariables()) + .containsKey("JAVA_HOME"); + assertThat(command.getArguments()) + // default settings + .contains(entry("sonar.web.javaOpts", "-Xmx512m -Xms128m -XX:+HeapDumpOnOutOfMemoryError")) + .contains(entry("sonar.cluster.enabled", "false")); + + assertThat(command.getSuppressedEnvVariables()).containsOnly("JAVA_TOOL_OPTIONS"); + } + + @Test + public void createWebCommand_configures_command_with_overridden_settings() throws Exception { + Properties props = new Properties(); + props.setProperty("sonar.web.port", "1234"); + props.setProperty("sonar.web.javaOpts", "-Xmx10G"); + JavaCommand command = newFactory(props).createWebCommand(true); + + assertThat(command.getJvmOptions().getAll()) + // enforced values + .contains("-Djava.awt.headless=true", "-Dfile.encoding=UTF-8") + // default settings + .contains("-Djava.io.tmpdir=" + tempDir.getAbsolutePath(), "-Dfile.encoding=UTF-8") + // overridden values + .contains("-Xmx10G") + .doesNotContain("-Xms128m", "-XX:+HeapDumpOnOutOfMemoryError"); + assertThat(command.getArguments()) + // default settings + .contains(entry("sonar.web.javaOpts", "-Xmx10G")) + .contains(entry("sonar.cluster.enabled", "false")); + + assertThat(command.getSuppressedEnvVariables()).containsOnly("JAVA_TOOL_OPTIONS"); + } + + @Test + public void createWebCommand_adds_configured_jdbc_driver_to_classpath() throws Exception { + Properties props = new Properties(); + File driverFile = temp.newFile(); + props.setProperty("sonar.jdbc.driverPath", driverFile.getAbsolutePath()); + + JavaCommand command = newFactory(props).createWebCommand(true); + + assertThat(command.getClasspath()) + .containsExactlyInAnyOrder("./lib/common/*", "./lib/server/*", driverFile.getAbsolutePath()); + } + + private void prepareEsFileSystem() throws IOException { + FileUtils.touch(new File(homeDir, "elasticsearch/bin/elasticsearch")); + FileUtils.touch(new File(homeDir, "elasticsearch/bin/elasticsearch.bat")); + } + + private CommandFactory newFactory(Properties userProps) throws IOException { + Properties p = new Properties(); + p.setProperty("sonar.path.home", homeDir.getAbsolutePath()); + p.setProperty("sonar.path.temp", tempDir.getAbsolutePath()); + p.setProperty("sonar.path.logs", logsDir.getAbsolutePath()); + p.putAll(userProps); + + Props props = new Props(p); + ProcessProperties.completeDefaults(props); + return new CommandFactoryImpl(props, tempDir, System2.INSTANCE); + } + + private <T> void attachMemoryAppenderToLoggerOf(Class<T> loggerClass) { + this.listAppender = ListAppender.attachMemoryAppenderToLoggerOf(loggerClass); + } + +} diff --git a/server/sonar-main/src/test/java/org/sonar/application/command/EsJvmOptionsTest.java b/server/sonar-main/src/test/java/org/sonar/application/command/EsJvmOptionsTest.java new file mode 100644 index 00000000000..9f4aa5efee3 --- /dev/null +++ b/server/sonar-main/src/test/java/org/sonar/application/command/EsJvmOptionsTest.java @@ -0,0 +1,108 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.command; + +import java.io.File; +import java.io.IOException; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; +import org.sonar.test.ExceptionCauseMatcher; + +import static org.assertj.core.api.Assertions.assertThat; + +public class EsJvmOptionsTest { + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void constructor_sets_mandatory_JVM_options() { + EsJvmOptions underTest = new EsJvmOptions(); + + assertThat(underTest.getAll()).containsExactly( + "-XX:+UseConcMarkSweepGC", + "-XX:CMSInitiatingOccupancyFraction=75", + "-XX:+UseCMSInitiatingOccupancyOnly", + "-XX:+AlwaysPreTouch", + "-server", + "-Xss1m", + "-Djava.awt.headless=true", + "-Dfile.encoding=UTF-8", + "-Djna.nosys=true", + "-Djdk.io.permissionsUseCanonicalPath=true", + "-Dio.netty.noUnsafe=true", + "-Dio.netty.noKeySetOptimization=true", + "-Dio.netty.recycler.maxCapacityPerThread=0", + "-Dlog4j.shutdownHookEnabled=false", + "-Dlog4j2.disable.jmx=true", + "-Dlog4j.skipJansi=true"); + } + + @Test + public void writeToJvmOptionFile_writes_all_JVM_options_to_file_with_warning_header() throws IOException { + File file = temporaryFolder.newFile(); + EsJvmOptions underTest = new EsJvmOptions() + .add("-foo") + .add("-bar"); + + underTest.writeToJvmOptionFile(file); + + assertThat(file).hasContent( + "# This file has been automatically generated by SonarQube during startup.\n" + + "# Please use sonar.search.javaOpts and/or sonar.search.javaAdditionalOpts in sonar.properties to specify jvm options for Elasticsearch\n" + + "\n" + + "# DO NOT EDIT THIS FILE\n" + + "\n" + + "-XX:+UseConcMarkSweepGC\n" + + "-XX:CMSInitiatingOccupancyFraction=75\n" + + "-XX:+UseCMSInitiatingOccupancyOnly\n" + + "-XX:+AlwaysPreTouch\n" + + "-server\n" + + "-Xss1m\n" + + "-Djava.awt.headless=true\n" + + "-Dfile.encoding=UTF-8\n" + + "-Djna.nosys=true\n" + + "-Djdk.io.permissionsUseCanonicalPath=true\n" + + "-Dio.netty.noUnsafe=true\n" + + "-Dio.netty.noKeySetOptimization=true\n" + + "-Dio.netty.recycler.maxCapacityPerThread=0\n" + + "-Dlog4j.shutdownHookEnabled=false\n" + + "-Dlog4j2.disable.jmx=true\n" + + "-Dlog4j.skipJansi=true\n" + + "-foo\n" + + "-bar"); + + } + + @Test + public void writeToJvmOptionFile_throws_ISE_in_case_of_IOException() throws IOException { + File notAFile = temporaryFolder.newFolder(); + EsJvmOptions underTest = new EsJvmOptions(); + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Cannot write Elasticsearch jvm options file"); + expectedException.expectCause(ExceptionCauseMatcher.hasType(IOException.class)); + + underTest.writeToJvmOptionFile(notAFile); + } +} diff --git a/server/sonar-main/src/test/java/org/sonar/application/command/JavaCommandTest.java b/server/sonar-main/src/test/java/org/sonar/application/command/JavaCommandTest.java new file mode 100644 index 00000000000..bd39967ccc9 --- /dev/null +++ b/server/sonar-main/src/test/java/org/sonar/application/command/JavaCommandTest.java @@ -0,0 +1,64 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.command; + +import java.io.File; +import java.util.Properties; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.sonar.process.ProcessId; + +import static org.assertj.core.api.Assertions.assertThat; + +public class JavaCommandTest { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Test + public void test_command_with_complete_information() throws Exception { + File workDir = temp.newFolder(); + JavaCommand<JvmOptions> command = new JavaCommand<>(ProcessId.ELASTICSEARCH, workDir); + + command.setArgument("first_arg", "val1"); + Properties args = new Properties(); + args.setProperty("second_arg", "val2"); + command.setArguments(args); + + command.setClassName("org.sonar.ElasticSearch"); + command.setEnvVariable("JAVA_COMMAND_TEST", "1000"); + command.addClasspath("lib/*.jar"); + command.addClasspath("conf/*.xml"); + JvmOptions<JvmOptions> jvmOptions = new JvmOptions<JvmOptions>() {}; + command.setJvmOptions(jvmOptions); + + assertThat(command.toString()).isNotNull(); + assertThat(command.getClasspath()).containsOnly("lib/*.jar", "conf/*.xml"); + assertThat(command.getJvmOptions()).isSameAs(jvmOptions); + assertThat(command.getWorkDir()).isSameAs(workDir); + assertThat(command.getClassName()).isEqualTo("org.sonar.ElasticSearch"); + + // copy current env variables + assertThat(command.getEnvVariables().get("JAVA_COMMAND_TEST")).isEqualTo("1000"); + assertThat(command.getEnvVariables().size()).isEqualTo(System.getenv().size() + 1); + } + +} diff --git a/server/sonar-main/src/test/java/org/sonar/application/command/JvmOptionsTest.java b/server/sonar-main/src/test/java/org/sonar/application/command/JvmOptionsTest.java new file mode 100644 index 00000000000..038506939a8 --- /dev/null +++ b/server/sonar-main/src/test/java/org/sonar/application/command/JvmOptionsTest.java @@ -0,0 +1,427 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.command; + +import com.google.common.collect.ImmutableMap; +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.Random; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.sonar.process.MessageException; +import org.sonar.process.Props; + +import static java.lang.String.valueOf; +import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; +import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +@RunWith(DataProviderRunner.class) +public class JvmOptionsTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private final Random random = new Random(); + private final String randomPropertyName = randomAlphanumeric(3); + private final String randomPrefix = "-" + randomAlphabetic(5).toLowerCase(Locale.ENGLISH); + private final String randomValue = randomAlphanumeric(4).toLowerCase(Locale.ENGLISH); + private final Properties properties = new Properties(); + private final JvmOptions underTest = new JvmOptions(); + + @Test + public void constructor_without_arguments_creates_empty_JvmOptions() { + JvmOptions<JvmOptions> testJvmOptions = new JvmOptions<>(); + + assertThat(testJvmOptions.getAll()).isEmpty(); + } + + @Test + public void constructor_throws_NPE_if_argument_is_null() { + expectJvmOptionNotNullNPE(); + + new JvmOptions(null); + } + + @Test + public void constructor_throws_NPE_if_any_option_prefix_is_null() { + Map<String, String> mandatoryJvmOptions = shuffleThenToMap( + Stream.of( + IntStream.range(0, random.nextInt(10)).mapToObj(i -> new Option("-B", valueOf(i))), + Stream.of(new Option(null, "value"))) + .flatMap(s -> s)); + + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("JVM option prefix can't be null"); + + new JvmOptions(mandatoryJvmOptions); + } + + @Test + @UseDataProvider("variousEmptyStrings") + public void constructor_throws_IAE_if_any_option_prefix_is_empty(String emptyString) { + Map<String, String> mandatoryJvmOptions = shuffleThenToMap( + Stream.of( + IntStream.range(0, random.nextInt(10)).mapToObj(i -> new Option("-B", valueOf(i))), + Stream.of(new Option(emptyString, "value"))) + .flatMap(s -> s)); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("JVM option prefix can't be empty"); + + new JvmOptions(mandatoryJvmOptions); + } + + @Test + public void constructor_throws_IAE_if_any_option_prefix_does_not_start_with_dash() { + String invalidPrefix = randomAlphanumeric(3); + Map<String, String> mandatoryJvmOptions = shuffleThenToMap( + Stream.of( + IntStream.range(0, random.nextInt(10)).mapToObj(i -> new Option("-B", valueOf(i))), + Stream.of(new Option(invalidPrefix, "value"))) + .flatMap(s -> s)); + + expectJvmOptionNotEmptyAndStartByDashIAE(); + + new JvmOptions(mandatoryJvmOptions); + } + + @Test + public void constructor_throws_NPE_if_any_option_value_is_null() { + Map<String, String> mandatoryJvmOptions = shuffleThenToMap( + Stream.of( + IntStream.range(0, random.nextInt(10)).mapToObj(i -> new Option("-B", valueOf(i))), + Stream.of(new Option("-prefix", null))) + .flatMap(s -> s)); + + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("JVM option value can't be null"); + + new JvmOptions(mandatoryJvmOptions); + } + + @Test + @UseDataProvider("variousEmptyStrings") + public void constructor_accepts_any_empty_option_value(String emptyString) { + Map<String, String> mandatoryJvmOptions = shuffleThenToMap( + Stream.of( + IntStream.range(0, random.nextInt(10)).mapToObj(i -> new Option("-B", valueOf(i))), + Stream.of(new Option("-prefix", emptyString))) + .flatMap(s -> s)); + + new JvmOptions(mandatoryJvmOptions); + } + + @Test + public void add_throws_NPE_if_argument_is_null() { + expectJvmOptionNotNullNPE(); + + underTest.add(null); + } + + @Test + @UseDataProvider("variousEmptyStrings") + public void add_throws_IAE_if_argument_is_empty(String emptyString) { + expectJvmOptionNotEmptyAndStartByDashIAE(); + + underTest.add(emptyString); + } + + @Test + public void add_throws_IAE_if_argument_does_not_start_with_dash() { + expectJvmOptionNotEmptyAndStartByDashIAE(); + + underTest.add(randomAlphanumeric(3)); + } + + @Test + @UseDataProvider("variousEmptyStrings") + public void add_adds_with_trimming(String emptyString) { + underTest.add(emptyString + "-foo" + emptyString); + + assertThat(underTest.getAll()).containsOnly("-foo"); + } + + @Test + public void add_throws_MessageException_if_option_starts_with_prefix_of_mandatory_option_but_has_different_value() { + String[] optionOverrides = { + randomPrefix, + randomPrefix + randomAlphanumeric(1), + randomPrefix + randomAlphanumeric(2), + randomPrefix + randomAlphanumeric(3), + randomPrefix + randomAlphanumeric(4), + randomPrefix + randomValue.substring(1), + randomPrefix + randomValue.substring(2), + randomPrefix + randomValue.substring(3) + }; + + JvmOptions underTest = new JvmOptions(ImmutableMap.of(randomPrefix, randomValue)); + + for (String optionOverride : optionOverrides) { + try { + underTest.add(optionOverride); + fail("an MessageException should have been thrown"); + } catch (MessageException e) { + assertThat(e.getMessage()).isEqualTo("a JVM option can't overwrite mandatory JVM options. " + optionOverride + " overwrites " + randomPrefix + randomValue); + } + } + } + + @Test + public void add_checks_against_mandatory_options_is_case_sensitive() { + String[] optionOverrides = { + randomPrefix, + randomPrefix + randomAlphanumeric(1), + randomPrefix + randomAlphanumeric(2), + randomPrefix + randomAlphanumeric(3), + randomPrefix + randomAlphanumeric(4), + randomPrefix + randomValue.substring(1), + randomPrefix + randomValue.substring(2), + randomPrefix + randomValue.substring(3) + }; + + JvmOptions underTest = new JvmOptions(ImmutableMap.of(randomPrefix, randomValue)); + + for (String optionOverride : optionOverrides) { + underTest.add(optionOverride.toUpperCase(Locale.ENGLISH)); + } + } + + @Test + public void add_accepts_property_equal_to_mandatory_option_and_does_not_add_it_twice() { + JvmOptions underTest = new JvmOptions(ImmutableMap.of(randomPrefix, randomValue)); + + underTest.add(randomPrefix + randomValue); + + assertThat(underTest.getAll()).containsOnly(randomPrefix + randomValue); + } + + @Test + public void addFromMandatoryProperty_fails_with_IAE_if_property_does_not_exist() { + expectMissingPropertyIAE(this.randomPropertyName); + + underTest.addFromMandatoryProperty(new Props(properties), this.randomPropertyName); + } + + @Test + public void addFromMandatoryProperty_fails_with_IAE_if_property_contains_an_empty_value() { + expectMissingPropertyIAE(this.randomPropertyName); + + underTest.addFromMandatoryProperty(new Props(properties), randomPropertyName); + } + + @Test + @UseDataProvider("variousEmptyStrings") + public void addFromMandatoryProperty_adds_single_option_of_property_with_trimming(String emptyString) { + properties.put(randomPropertyName, emptyString + "-foo" + emptyString); + + underTest.addFromMandatoryProperty(new Props(properties), randomPropertyName); + + assertThat(underTest.getAll()).containsOnly("-foo"); + } + + @Test + @UseDataProvider("variousEmptyStrings") + public void addFromMandatoryProperty_fails_with_MessageException_if_property_does_not_start_with_dash_after_trimmed(String emptyString) { + properties.put(randomPropertyName, emptyString + "foo -bar"); + + expectJvmOptionNotEmptyAndStartByDashMessageException(randomPropertyName, "foo"); + + underTest.addFromMandatoryProperty(new Props(properties), randomPropertyName); + } + + @Test + @UseDataProvider("variousEmptyStrings") + public void addFromMandatoryProperty_adds_options_of_property_with_trimming(String emptyString) { + properties.put(randomPropertyName, emptyString + "-foo" + emptyString + " -bar" + emptyString + " -duck" + emptyString); + + underTest.addFromMandatoryProperty(new Props(properties), randomPropertyName); + + assertThat(underTest.getAll()).containsOnly("-foo", "-bar", "-duck"); + } + + @Test + public void addFromMandatoryProperty_supports_spaces_inside_options() { + properties.put(randomPropertyName, "-foo bar -duck"); + + underTest.addFromMandatoryProperty(new Props(properties), randomPropertyName); + + assertThat(underTest.getAll()).containsOnly("-foo bar", "-duck"); + } + + @Test + public void addFromMandatoryProperty_throws_IAE_if_option_starts_with_prefix_of_mandatory_option_but_has_different_value() { + String[] optionOverrides = { + randomPrefix, + randomPrefix + randomValue.substring(1), + randomPrefix + randomValue.substring(1), + randomPrefix + randomValue.substring(2), + randomPrefix + randomValue.substring(3), + randomPrefix + randomValue.substring(3) + randomAlphanumeric(1), + randomPrefix + randomValue.substring(3) + randomAlphanumeric(2), + randomPrefix + randomValue.substring(3) + randomAlphanumeric(3), + randomPrefix + randomValue + randomAlphanumeric(1) + }; + + JvmOptions underTest = new JvmOptions(ImmutableMap.of(randomPrefix, randomValue)); + + for (String optionOverride : optionOverrides) { + try { + properties.put(randomPropertyName, optionOverride); + underTest.addFromMandatoryProperty(new Props(properties), randomPropertyName); + fail("an MessageException should have been thrown"); + } catch (MessageException e) { + assertThat(e.getMessage()) + .isEqualTo("a JVM option can't overwrite mandatory JVM options. " + + "The following JVM options defined by property '" + randomPropertyName + "' are invalid: " + optionOverride + " overwrites " + randomPrefix + randomValue); + } + } + } + + @Test + public void addFromMandatoryProperty_checks_against_mandatory_options_is_case_sensitive() { + String[] optionOverrides = { + randomPrefix, + randomPrefix + randomValue.substring(1), + randomPrefix + randomValue.substring(1), + randomPrefix + randomValue.substring(2), + randomPrefix + randomValue.substring(3), + randomPrefix + randomValue.substring(3) + randomAlphanumeric(1), + randomPrefix + randomValue.substring(3) + randomAlphanumeric(2), + randomPrefix + randomValue.substring(3) + randomAlphanumeric(3), + randomPrefix + randomValue + randomAlphanumeric(1) + }; + + JvmOptions underTest = new JvmOptions(ImmutableMap.of(randomPrefix, randomValue)); + + for (String optionOverride : optionOverrides) { + properties.setProperty(randomPropertyName, optionOverride.toUpperCase(Locale.ENGLISH)); + underTest.addFromMandatoryProperty(new Props(properties), randomPropertyName); + } + } + + @Test + public void addFromMandatoryProperty_reports_all_overriding_options_in_single_exception() { + String overriding1 = randomPrefix; + String overriding2 = randomPrefix + randomValue + randomAlphanumeric(1); + properties.setProperty(randomPropertyName, "-foo " + overriding1 + " -bar " + overriding2); + + JvmOptions underTest = new JvmOptions(ImmutableMap.of(randomPrefix, randomValue)); + + expectedException.expect(MessageException.class); + expectedException.expectMessage("a JVM option can't overwrite mandatory JVM options. " + + "The following JVM options defined by property '" + randomPropertyName + "' are invalid: " + + overriding1 + " overwrites " + randomPrefix + randomValue + ", " + overriding2 + " overwrites " + randomPrefix + randomValue); + + underTest.addFromMandatoryProperty(new Props(properties), randomPropertyName); + } + + @Test + public void addFromMandatoryProperty_accepts_property_equal_to_mandatory_option_and_does_not_add_it_twice() { + JvmOptions underTest = new JvmOptions(ImmutableMap.of(randomPrefix, randomValue)); + + properties.put(randomPropertyName, randomPrefix + randomValue); + underTest.addFromMandatoryProperty(new Props(properties), randomPropertyName); + + assertThat(underTest.getAll()).containsOnly(randomPrefix + randomValue); + } + + @Test + public void toString_prints_all_jvm_options() { + underTest.add("-foo").add("-bar"); + + assertThat(underTest.toString()).isEqualTo("[-foo, -bar]"); + } + + private void expectJvmOptionNotNullNPE() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("a JVM option can't be null"); + } + + private void expectJvmOptionNotEmptyAndStartByDashIAE() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("a JVM option can't be empty and must start with '-'"); + } + + private void expectJvmOptionNotEmptyAndStartByDashMessageException(String randomPropertyName, String option) { + expectedException.expect(MessageException.class); + expectedException.expectMessage("a JVM option can't be empty and must start with '-'. " + + "The following JVM options defined by property '" + randomPropertyName + "' are invalid: " + option); + } + + public void expectMissingPropertyIAE(String randomPropertyName) { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Missing property: " + randomPropertyName); + } + + @DataProvider() + public static Object[][] variousEmptyStrings() { + return new Object[][] { + {""}, + {" "}, + {" "} + }; + } + + private static Map<String, String> shuffleThenToMap(Stream<Option> stream) { + List<Option> options = stream.collect(Collectors.toList()); + Collections.shuffle(options); + Map<String, String> res = new HashMap<>(options.size()); + for (Option option : options) { + res.put(option.getPrefix(), option.getValue()); + } + return res; + } + + private static final class Option { + private final String prefix; + private final String value; + + private Option(String prefix, String value) { + this.prefix = prefix; + this.value = value; + } + + public String getPrefix() { + return prefix; + } + + public String getValue() { + return value; + } + + @Override + public String toString() { + return "[" + prefix + "-" + value + ']'; + } + } +} diff --git a/server/sonar-main/src/test/java/org/sonar/application/command/WebJvmOptionsTest.java b/server/sonar-main/src/test/java/org/sonar/application/command/WebJvmOptionsTest.java new file mode 100644 index 00000000000..dfd8f154e6a --- /dev/null +++ b/server/sonar-main/src/test/java/org/sonar/application/command/WebJvmOptionsTest.java @@ -0,0 +1,43 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.command; + +import java.io.File; +import java.io.IOException; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import static org.assertj.core.api.Assertions.assertThat; + +public class WebJvmOptionsTest { + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void constructor_sets_mandatory_JVM_options() throws IOException { + File tmpDir = temporaryFolder.newFolder(); + WebJvmOptions underTest = new WebJvmOptions(tmpDir); + + assertThat(underTest.getAll()).containsExactly( + "-Djava.awt.headless=true", "-Dfile.encoding=UTF-8", "-Djava.io.tmpdir=" + tmpDir.getAbsolutePath()); + } + +} diff --git a/server/sonar-main/src/test/java/org/sonar/application/es/EsFileSystemTest.java b/server/sonar-main/src/test/java/org/sonar/application/es/EsFileSystemTest.java new file mode 100644 index 00000000000..e015b6bcce3 --- /dev/null +++ b/server/sonar-main/src/test/java/org/sonar/application/es/EsFileSystemTest.java @@ -0,0 +1,187 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.IOException; +import java.util.Properties; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; +import org.sonar.process.ProcessProperties; +import org.sonar.process.Props; + +import static org.assertj.core.api.Assertions.assertThat; + +public class EsFileSystemTest { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void constructor_fails_with_IAE_if_sq_home_property_is_not_defined() { + Props props = new Props(new Properties()); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Property sonar.path.home is not set"); + + new EsFileSystem(props); + } + + @Test + public void constructor_fails_with_IAE_if_temp_dir_property_is_not_defined() throws IOException { + Props props = new Props(new Properties()); + props.set(ProcessProperties.PATH_HOME, temp.newFolder().getAbsolutePath()); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Property sonar.path.temp is not set"); + + new EsFileSystem(props); + } + + @Test + public void getHomeDirectory_is_elasticsearch_subdirectory_of_sq_home_directory() throws IOException { + File sqHomeDir = temp.newFolder(); + Props props = new Props(new Properties()); + props.set(ProcessProperties.PATH_HOME, sqHomeDir.getAbsolutePath()); + props.set(ProcessProperties.PATH_TEMP, temp.newFolder().getAbsolutePath()); + props.set(ProcessProperties.PATH_LOGS, temp.newFolder().getAbsolutePath()); + + EsFileSystem underTest = new EsFileSystem(props); + + assertThat(underTest.getHomeDirectory()).isEqualTo(new File(sqHomeDir, "elasticsearch")); + } + + @Test + public void getDataDirectory_is_data_es_subdirectory_of_sq_home_directory_by_default() throws IOException { + File sqHomeDir = temp.newFolder(); + Props props = new Props(new Properties()); + props.set(ProcessProperties.PATH_HOME, sqHomeDir.getAbsolutePath()); + props.set(ProcessProperties.PATH_TEMP, temp.newFolder().getAbsolutePath()); + props.set(ProcessProperties.PATH_LOGS, temp.newFolder().getAbsolutePath()); + + EsFileSystem underTest = new EsFileSystem(props); + + assertThat(underTest.getDataDirectory()).isEqualTo(new File(sqHomeDir, "data/es")); + } + + @Test + public void override_data_dir() throws Exception { + File sqHomeDir = temp.newFolder(); + File tempDir = temp.newFolder(); + File dataDir = temp.newFolder(); + Props props = new Props(new Properties()); + props.set(ProcessProperties.PATH_HOME, sqHomeDir.getAbsolutePath()); + props.set(ProcessProperties.PATH_TEMP, tempDir.getAbsolutePath()); + props.set(ProcessProperties.PATH_LOGS, temp.newFolder().getAbsolutePath()); + + props.set(ProcessProperties.PATH_DATA, dataDir.getAbsolutePath()); + + EsFileSystem underTest = new EsFileSystem(props); + + assertThat(underTest.getDataDirectory()).isEqualTo(new File(dataDir, "es")); + } + + @Test + public void getLogDirectory_is_configured_with_non_nullable_PATH_LOG_variable() throws IOException { + File sqHomeDir = temp.newFolder(); + File logDir = temp.newFolder(); + Props props = new Props(new Properties()); + props.set(ProcessProperties.PATH_HOME, sqHomeDir.getAbsolutePath()); + props.set(ProcessProperties.PATH_TEMP, temp.newFolder().getAbsolutePath()); + props.set(ProcessProperties.PATH_LOGS, logDir.getAbsolutePath()); + + EsFileSystem underTest = new EsFileSystem(props); + + assertThat(underTest.getLogDirectory()).isEqualTo(logDir); + } + + @Test + public void conf_directory_is_conf_es_subdirectory_of_sq_temp_directory() throws IOException { + File tempDir = temp.newFolder(); + Props props = new Props(new Properties()); + props.set(ProcessProperties.PATH_HOME, temp.newFolder().getAbsolutePath()); + props.set(ProcessProperties.PATH_TEMP, tempDir.getAbsolutePath()); + props.set(ProcessProperties.PATH_LOGS, temp.newFolder().getAbsolutePath()); + + EsFileSystem underTest = new EsFileSystem(props); + + assertThat(underTest.getConfDirectory()).isEqualTo(new File(tempDir, "conf/es")); + } + + @Test + public void getExecutable_resolve_executable_for_platform() throws IOException { + File sqHomeDir = temp.newFolder(); + Props props = new Props(new Properties()); + props.set(ProcessProperties.PATH_HOME, sqHomeDir.getAbsolutePath()); + props.set(ProcessProperties.PATH_TEMP, temp.newFolder().getAbsolutePath()); + props.set(ProcessProperties.PATH_LOGS, temp.newFolder().getAbsolutePath()); + + EsFileSystem underTest = new EsFileSystem(props); + + if (System.getProperty("os.name").startsWith("Windows")) { + assertThat(underTest.getExecutable()).isEqualTo(new File(sqHomeDir, "elasticsearch/bin/elasticsearch.bat")); + } else { + assertThat(underTest.getExecutable()).isEqualTo(new File(sqHomeDir, "elasticsearch/bin/elasticsearch")); + } + } + + @Test + public void getLog4j2Properties_is_in_es_conf_directory() throws IOException { + File tempDir = temp.newFolder(); + Props props = new Props(new Properties()); + props.set(ProcessProperties.PATH_HOME, temp.newFolder().getAbsolutePath()); + props.set(ProcessProperties.PATH_TEMP, tempDir.getAbsolutePath()); + props.set(ProcessProperties.PATH_LOGS, temp.newFolder().getAbsolutePath()); + + EsFileSystem underTest = new EsFileSystem(props); + + assertThat(underTest.getLog4j2Properties()).isEqualTo(new File(tempDir, "conf/es/log4j2.properties")); + } + + @Test + public void getElasticsearchYml_is_in_es_conf_directory() throws IOException { + File tempDir = temp.newFolder(); + Props props = new Props(new Properties()); + props.set(ProcessProperties.PATH_HOME, temp.newFolder().getAbsolutePath()); + props.set(ProcessProperties.PATH_TEMP, tempDir.getAbsolutePath()); + props.set(ProcessProperties.PATH_LOGS, temp.newFolder().getAbsolutePath()); + + EsFileSystem underTest = new EsFileSystem(props); + + assertThat(underTest.getElasticsearchYml()).isEqualTo(new File(tempDir, "conf/es/elasticsearch.yml")); + } + + @Test + public void getJvmOptions_is_in_es_conf_directory() throws IOException { + File tempDir = temp.newFolder(); + Props props = new Props(new Properties()); + props.set(ProcessProperties.PATH_HOME, temp.newFolder().getAbsolutePath()); + props.set(ProcessProperties.PATH_TEMP, tempDir.getAbsolutePath()); + props.set(ProcessProperties.PATH_LOGS, temp.newFolder().getAbsolutePath()); + + EsFileSystem underTest = new EsFileSystem(props); + + assertThat(underTest.getJvmOptions()).isEqualTo(new File(tempDir, "conf/es/jvm.options")); + } +} diff --git a/server/sonar-main/src/test/java/org/sonar/application/es/EsLoggingTest.java b/server/sonar-main/src/test/java/org/sonar/application/es/EsLoggingTest.java new file mode 100644 index 00000000000..7dc29ec2f55 --- /dev/null +++ b/server/sonar-main/src/test/java/org/sonar/application/es/EsLoggingTest.java @@ -0,0 +1,131 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.IOException; +import java.util.HashSet; +import java.util.Properties; +import java.util.Set; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.sonar.process.Props; + +import static org.assertj.core.api.Assertions.assertThat; + +public class EsLoggingTest { + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private EsLogging underTest = new EsLogging(); + + @Test + public void createProperties_with_empty_props() throws IOException { + File logDir = temporaryFolder.newFolder(); + Properties properties = underTest.createProperties(newProps(), logDir); + + verifyProperties(properties, + "status", "ERROR", + "appender.file_es.type", "RollingFile", + "appender.file_es.name", "file_es", + "appender.file_es.filePattern", new File(logDir, "es.%d{yyyy-MM-dd}.log").getAbsolutePath(), + "appender.file_es.fileName", new File(logDir, "es.log").getAbsolutePath(), + "appender.file_es.layout.type", "PatternLayout", + "appender.file_es.layout.pattern", "%d{yyyy.MM.dd HH:mm:ss} %-5level es[][%logger{1.}] %msg%n", + "appender.file_es.policies.type", "Policies", + "appender.file_es.policies.time.type", "TimeBasedTriggeringPolicy", + "appender.file_es.policies.time.interval", "1", + "appender.file_es.policies.time.modulate", "true", + "appender.file_es.strategy.type", "DefaultRolloverStrategy", + "appender.file_es.strategy.fileIndex", "nomax", + "appender.file_es.strategy.action.type", "Delete", + "appender.file_es.strategy.action.basepath", logDir.getAbsolutePath(), + "appender.file_es.strategy.action.maxDepth", "1", + "appender.file_es.strategy.action.condition.type", "IfFileName", + "appender.file_es.strategy.action.condition.glob", "es*", + "appender.file_es.strategy.action.condition.nested_condition.type", "IfAccumulatedFileCount", + "appender.file_es.strategy.action.condition.nested_condition.exceeds", "7", + "rootLogger.level", "INFO", + "rootLogger.appenderRef.file_es.ref", "file_es"); + } + + @Test + public void createProperties_sets_root_logger_to_INFO_if_no_property_is_set() throws IOException { + File logDir = temporaryFolder.newFolder(); + Properties properties = underTest.createProperties(newProps(), logDir); + + assertThat(properties.getProperty("rootLogger.level")).isEqualTo("INFO"); + } + + @Test + public void createProperties_sets_root_logger_to_global_property_if_set() throws IOException { + File logDir = temporaryFolder.newFolder(); + Properties properties = underTest.createProperties(newProps("sonar.log.level", "TRACE"), logDir); + + assertThat(properties.getProperty("rootLogger.level")).isEqualTo("TRACE"); + } + + @Test + public void createProperties_sets_root_logger_to_process_property_if_set() throws IOException { + File logDir = temporaryFolder.newFolder(); + Properties properties = underTest.createProperties(newProps("sonar.log.level.es", "DEBUG"), logDir); + + assertThat(properties.getProperty("rootLogger.level")).isEqualTo("DEBUG"); + } + + @Test + public void createProperties_sets_root_logger_to_process_property_over_global_property_if_both_set() throws IOException { + File logDir = temporaryFolder.newFolder(); + Properties properties = underTest.createProperties( + newProps( + "sonar.log.level", "DEBUG", + "sonar.log.level.es", "TRACE"), + logDir); + + assertThat(properties.getProperty("rootLogger.level")).isEqualTo("TRACE"); + } + + private static Props newProps(String... propertyKeysAndValues) { + assertThat(propertyKeysAndValues.length % 2).describedAs("Number of parameters must be even").isEqualTo(0); + Properties properties = new Properties(); + for (int i = 0; i < propertyKeysAndValues.length; i++) { + properties.put(propertyKeysAndValues[i++], propertyKeysAndValues[i]); + } + return new Props(properties); + } + + private void verifyProperties(Properties properties, String... expectedPropertyKeysAndValuesOrdered) { + if (expectedPropertyKeysAndValuesOrdered.length == 0) { + assertThat(properties.size()).isEqualTo(0); + } else { + assertThat(expectedPropertyKeysAndValuesOrdered.length % 2).describedAs("Number of parameters must be even").isEqualTo(0); + Set<String> keys = new HashSet<>(expectedPropertyKeysAndValuesOrdered.length / 2 + 1); + keys.add("status"); + for (int i = 0; i < expectedPropertyKeysAndValuesOrdered.length; i++) { + String key = expectedPropertyKeysAndValuesOrdered[i++]; + String value = expectedPropertyKeysAndValuesOrdered[i]; + assertThat(properties.get(key)).describedAs("Unexpected value for property " + key).isEqualTo(value); + keys.add(key); + } + assertThat(properties.keySet()).containsOnly(keys.toArray()); + } + } +} diff --git a/server/sonar-main/src/test/java/org/sonar/application/es/EsSettingsTest.java b/server/sonar-main/src/test/java/org/sonar/application/es/EsSettingsTest.java new file mode 100644 index 00000000000..1a09f0ac240 --- /dev/null +++ b/server/sonar-main/src/test/java/org/sonar/application/es/EsSettingsTest.java @@ -0,0 +1,302 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 ch.qos.logback.classic.spi.ILoggingEvent; +import java.io.File; +import java.io.IOException; +import java.util.Map; +import java.util.Properties; +import java.util.Random; +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; +import org.sonar.application.logging.ListAppender; +import org.sonar.cluster.ClusterProperties; +import org.sonar.process.ProcessProperties; +import org.sonar.process.Props; +import org.sonar.process.System2; + +import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.sonar.cluster.ClusterProperties.CLUSTER_NAME; +import static org.sonar.cluster.ClusterProperties.CLUSTER_SEARCH_HOSTS; + +public class EsSettingsTest { + + private static final boolean CLUSTER_ENABLED = true; + private static final boolean CLUSTER_DISABLED = false; + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + private ListAppender listAppender; + + @After + public void tearDown() throws Exception { + if (listAppender != null) { + ListAppender.detachMemoryAppenderToLoggerOf(EsSettings.class, listAppender); + } + } + + @Test + public void constructor_does_not_logs_warning_if_env_variable_ES_JVM_OPTIONS_is_not_set() { + this.listAppender = ListAppender.attachMemoryAppenderToLoggerOf(EsSettings.class); + Props props = minimalProps(); + System2 system2 = mock(System2.class); + new EsSettings(props, new EsFileSystem(props), system2); + + assertThat(listAppender.getLogs()).isEmpty(); + } + + @Test + public void constructor_does_not_logs_warning_if_env_variable_ES_JVM_OPTIONS_is_set_and_empty() { + this.listAppender = ListAppender.attachMemoryAppenderToLoggerOf(EsSettings.class); + Props props = minimalProps(); + System2 system2 = mock(System2.class); + when(system2.getenv("ES_JVM_OPTIONS")).thenReturn(" "); + new EsSettings(props, new EsFileSystem(props), system2); + + assertThat(listAppender.getLogs()).isEmpty(); + } + + @Test + public void constructor_logs_warning_if_env_variable_ES_JVM_OPTIONS_is_set_and_non_empty() throws IOException { + this.listAppender = ListAppender.attachMemoryAppenderToLoggerOf(EsSettings.class); + Props props = minimalProps(); + System2 system2 = mock(System2.class); + when(system2.getenv("ES_JVM_OPTIONS")).thenReturn(randomAlphanumeric(2)); + new EsSettings(props, new EsFileSystem(props), system2); + + assertThat(listAppender.getLogs()) + .extracting(ILoggingEvent::getMessage) + .containsOnly("ES_JVM_OPTIONS is defined but will be ignored. " + + "Use sonar.search.javaOpts and/or sonar.search.javaAdditionalOpts in sonar.properties to specify jvm options for Elasticsearch"); + } + + private Props minimalProps() { + Props props = new Props(new Properties()); + props.set(ProcessProperties.PATH_HOME, randomAlphanumeric(12)); + props.set(ProcessProperties.PATH_TEMP, randomAlphanumeric(12)); + props.set(ProcessProperties.PATH_LOGS, randomAlphanumeric(12)); + props.set(CLUSTER_NAME, randomAlphanumeric(12)); + return props; + } + + @Test + public void test_default_settings_for_standalone_mode() throws Exception { + File homeDir = temp.newFolder(); + Props props = new Props(new Properties()); + props.set(ProcessProperties.SEARCH_PORT, "1234"); + props.set(ProcessProperties.SEARCH_HOST, "127.0.0.1"); + props.set(ProcessProperties.PATH_HOME, homeDir.getAbsolutePath()); + props.set(ProcessProperties.PATH_TEMP, temp.newFolder().getAbsolutePath()); + props.set(ProcessProperties.PATH_LOGS, temp.newFolder().getAbsolutePath()); + props.set(CLUSTER_NAME, "sonarqube"); + + EsSettings esSettings = new EsSettings(props, new EsFileSystem(props), System2.INSTANCE); + + Map<String, String> generated = esSettings.build(); + assertThat(generated.get("transport.tcp.port")).isEqualTo("1234"); + assertThat(generated.get("transport.host")).isEqualTo("127.0.0.1"); + + // no cluster, but cluster and node names are set though + assertThat(generated.get("cluster.name")).isEqualTo("sonarqube"); + assertThat(generated.get("node.name")).isEqualTo("sonarqube"); + + assertThat(generated.get("path.data")).isNotNull(); + assertThat(generated.get("path.logs")).isNotNull(); + assertThat(generated.get("path.home")).isNull(); + assertThat(generated.get("path.conf")).isNotNull(); + + // http is disabled for security reasons + assertThat(generated.get("http.enabled")).isEqualTo("false"); + + assertThat(generated.get("discovery.zen.ping.unicast.hosts")).isNull(); + assertThat(generated.get("discovery.zen.minimum_master_nodes")).isEqualTo("1"); + assertThat(generated.get("discovery.initial_state_timeout")).isEqualTo("30s"); + + assertThat(generated.get("action.auto_create_index")).isEqualTo("false"); + } + + @Test + public void test_default_settings_for_cluster_mode() throws Exception { + File homeDir = temp.newFolder(); + Props props = new Props(new Properties()); + props.set(ProcessProperties.SEARCH_PORT, "1234"); + props.set(ProcessProperties.SEARCH_HOST, "127.0.0.1"); + props.set(ProcessProperties.PATH_HOME, homeDir.getAbsolutePath()); + props.set(ProcessProperties.PATH_TEMP, temp.newFolder().getAbsolutePath()); + props.set(ProcessProperties.PATH_LOGS, temp.newFolder().getAbsolutePath()); + props.set(ClusterProperties.CLUSTER_NAME, "sonarqube-1"); + props.set(ClusterProperties.CLUSTER_ENABLED, "true"); + props.set(ClusterProperties.CLUSTER_NODE_NAME, "node-1"); + + EsSettings esSettings = new EsSettings(props, new EsFileSystem(props), System2.INSTANCE); + + Map<String, String> generated = esSettings.build(); + assertThat(generated.get("cluster.name")).isEqualTo("sonarqube-1"); + assertThat(generated.get("node.name")).isEqualTo("node-1"); + } + + @Test + public void test_node_name_default_for_cluster_mode() throws Exception { + File homeDir = temp.newFolder(); + Props props = new Props(new Properties()); + props.set(ClusterProperties.CLUSTER_NAME, "sonarqube"); + props.set(ClusterProperties.CLUSTER_ENABLED, "true"); + props.set(ProcessProperties.SEARCH_PORT, "1234"); + props.set(ProcessProperties.SEARCH_HOST, "127.0.0.1"); + props.set(ProcessProperties.PATH_HOME, homeDir.getAbsolutePath()); + props.set(ProcessProperties.PATH_TEMP, temp.newFolder().getAbsolutePath()); + props.set(ProcessProperties.PATH_LOGS, temp.newFolder().getAbsolutePath()); + EsSettings esSettings = new EsSettings(props, new EsFileSystem(props), System2.INSTANCE); + Map<String, String> generated = esSettings.build(); + assertThat(generated.get("node.name")).startsWith("sonarqube-"); + } + + @Test + public void test_node_name_default_for_standalone_mode() throws Exception { + File homeDir = temp.newFolder(); + Props props = new Props(new Properties()); + props.set(ClusterProperties.CLUSTER_NAME, "sonarqube"); + props.set(ClusterProperties.CLUSTER_ENABLED, "false"); + props.set(ProcessProperties.SEARCH_PORT, "1234"); + props.set(ProcessProperties.SEARCH_HOST, "127.0.0.1"); + props.set(ProcessProperties.PATH_HOME, homeDir.getAbsolutePath()); + props.set(ProcessProperties.PATH_TEMP, temp.newFolder().getAbsolutePath()); + props.set(ProcessProperties.PATH_LOGS, temp.newFolder().getAbsolutePath()); + EsSettings esSettings = new EsSettings(props, new EsFileSystem(props), System2.INSTANCE); + Map<String, String> generated = esSettings.build(); + assertThat(generated.get("node.name")).isEqualTo("sonarqube"); + } + + @Test + public void path_properties_are_values_from_EsFileSystem_argument() throws IOException { + EsFileSystem mockedEsFileSystem = mock(EsFileSystem.class); + when(mockedEsFileSystem.getHomeDirectory()).thenReturn(new File("/foo/home")); + when(mockedEsFileSystem.getConfDirectory()).thenReturn(new File("/foo/conf")); + when(mockedEsFileSystem.getLogDirectory()).thenReturn(new File("/foo/log")); + when(mockedEsFileSystem.getDataDirectory()).thenReturn(new File("/foo/data")); + + EsSettings underTest = new EsSettings(minProps(new Random().nextBoolean()), mockedEsFileSystem, System2.INSTANCE); + + Map<String, String> generated = underTest.build(); + assertThat(generated.get("path.data")).isEqualTo("/foo/data"); + assertThat(generated.get("path.logs")).isEqualTo("/foo/log"); + assertThat(generated.get("path.conf")).isEqualTo("/foo/conf"); + } + + @Test + public void set_discovery_settings_if_cluster_is_enabled() throws Exception { + Props props = minProps(CLUSTER_ENABLED); + props.set(CLUSTER_SEARCH_HOSTS, "1.2.3.4:9000,1.2.3.5:8080"); + Map<String, String> settings = new EsSettings(props, new EsFileSystem(props), System2.INSTANCE).build(); + + assertThat(settings.get("discovery.zen.ping.unicast.hosts")).isEqualTo("1.2.3.4:9000,1.2.3.5:8080"); + assertThat(settings.get("discovery.zen.minimum_master_nodes")).isEqualTo("2"); + assertThat(settings.get("discovery.initial_state_timeout")).isEqualTo("120s"); + } + + @Test + public void incorrect_values_of_minimum_master_nodes() throws Exception { + Props props = minProps(CLUSTER_ENABLED); + props.set(ProcessProperties.SEARCH_MINIMUM_MASTER_NODES, "ꝱꝲꝳପ"); + + EsSettings underTest = new EsSettings(props, new EsFileSystem(props), System2.INSTANCE); + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Value of property sonar.search.minimumMasterNodes is not an integer:"); + underTest.build(); + } + + @Test + public void cluster_is_enabled_with_defined_minimum_master_nodes() throws Exception { + Props props = minProps(CLUSTER_ENABLED); + props.set(ProcessProperties.SEARCH_MINIMUM_MASTER_NODES, "5"); + Map<String, String> settings = new EsSettings(props, new EsFileSystem(props), System2.INSTANCE).build(); + + assertThat(settings.get("discovery.zen.minimum_master_nodes")).isEqualTo("5"); + } + + @Test + public void cluster_is_enabled_with_defined_initialTimeout() throws Exception { + Props props = minProps(CLUSTER_ENABLED); + props.set(ProcessProperties.SEARCH_INITIAL_STATE_TIMEOUT, "10s"); + Map<String, String> settings = new EsSettings(props, new EsFileSystem(props), System2.INSTANCE).build(); + + assertThat(settings.get("discovery.initial_state_timeout")).isEqualTo("10s"); + } + + @Test + public void in_standalone_initialTimeout_is_not_overridable() throws Exception { + Props props = minProps(CLUSTER_DISABLED); + props.set(ProcessProperties.SEARCH_INITIAL_STATE_TIMEOUT, "10s"); + Map<String, String> settings = new EsSettings(props, new EsFileSystem(props), System2.INSTANCE).build(); + + assertThat(settings.get("discovery.initial_state_timeout")).isEqualTo("30s"); + } + + @Test + public void in_standalone_minimumMasterNodes_is_not_overridable() throws Exception { + Props props = minProps(CLUSTER_DISABLED); + props.set(ProcessProperties.SEARCH_MINIMUM_MASTER_NODES, "5"); + Map<String, String> settings = new EsSettings(props, new EsFileSystem(props), System2.INSTANCE).build(); + + assertThat(settings.get("discovery.zen.minimum_master_nodes")).isEqualTo("1"); + } + + @Test + public void enable_http_connector() throws Exception { + Props props = minProps(CLUSTER_DISABLED); + props.set(ProcessProperties.SEARCH_HTTP_PORT, "9010"); + Map<String, String> settings = new EsSettings(props, new EsFileSystem(props), System2.INSTANCE).build(); + + assertThat(settings.get("http.port")).isEqualTo("9010"); + assertThat(settings.get("http.host")).isEqualTo("127.0.0.1"); + assertThat(settings.get("http.enabled")).isEqualTo("true"); + } + + @Test + public void enable_http_connector_different_host() throws Exception { + Props props = minProps(CLUSTER_DISABLED); + props.set(ProcessProperties.SEARCH_HTTP_PORT, "9010"); + props.set(ProcessProperties.SEARCH_HOST, "127.0.0.2"); + Map<String, String> settings = new EsSettings(props, new EsFileSystem(props), System2.INSTANCE).build(); + + assertThat(settings.get("http.port")).isEqualTo("9010"); + assertThat(settings.get("http.host")).isEqualTo("127.0.0.2"); + assertThat(settings.get("http.enabled")).isEqualTo("true"); + } + + private Props minProps(boolean cluster) throws IOException { + File homeDir = temp.newFolder(); + Props props = new Props(new Properties()); + ProcessProperties.completeDefaults(props); + props.set(ProcessProperties.PATH_HOME, homeDir.getAbsolutePath()); + props.set(ClusterProperties.CLUSTER_ENABLED, Boolean.toString(cluster)); + return props; + } +} diff --git a/server/sonar-main/src/test/java/org/sonar/application/es/EsYmlSettingsTest.java b/server/sonar-main/src/test/java/org/sonar/application/es/EsYmlSettingsTest.java new file mode 100644 index 00000000000..6920c7f2710 --- /dev/null +++ b/server/sonar-main/src/test/java/org/sonar/application/es/EsYmlSettingsTest.java @@ -0,0 +1,62 @@ +package org.sonar.application.es;/* + * SonarQube + * Copyright (C) 2009-2017 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. + */ + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; + +import static org.assertj.core.api.Assertions.assertThat; + +public class EsYmlSettingsTest { + + @Rule + public TemporaryFolder folder= new TemporaryFolder(); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void test_generation_of_file() throws IOException { + File yamlFile = folder.newFile(); + new EsYmlSettings(new HashMap<>()).writeToYmlSettingsFile(yamlFile); + + assertThat(yamlFile).exists(); + assertThat(yamlFile).hasContent("# This file has been automatically generated by SonarQube during startup.\n" + + "\n" + + "# DO NOT EDIT THIS FILE\n" + + "\n" + + "{\n" + + " }"); + } + + @Test + public void if_file_is_not_writable_ISE_must_be_thrown() throws IOException { + File yamlFile = folder.newFile(); + yamlFile.setReadOnly(); + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Cannot write Elasticsearch yml settings file"); + + new EsYmlSettings(new HashMap<>()).writeToYmlSettingsFile(yamlFile); + } +} diff --git a/server/sonar-main/src/test/java/org/sonar/application/logging/ListAppender.java b/server/sonar-main/src/test/java/org/sonar/application/logging/ListAppender.java new file mode 100644 index 00000000000..7a8ca8bd56a --- /dev/null +++ b/server/sonar-main/src/test/java/org/sonar/application/logging/ListAppender.java @@ -0,0 +1,53 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.logging; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; +import java.util.ArrayList; +import java.util.List; +import org.sonar.process.logging.LogbackHelper; + +public final class ListAppender extends AppenderBase<ILoggingEvent> { + private final List<ILoggingEvent> logs = new ArrayList<>(); + + @Override + protected void append(ILoggingEvent eventObject) { + logs.add(eventObject); + } + + public List<ILoggingEvent> getLogs() { + return logs; + } + + public static <T> ListAppender attachMemoryAppenderToLoggerOf(Class<T> loggerClass) { + ListAppender listAppender = new ListAppender(); + new LogbackHelper().getRootContext().getLogger(loggerClass) + .addAppender(listAppender); + listAppender.start(); + return listAppender; + } + + public static <T> void detachMemoryAppenderToLoggerOf(Class<T> loggerClass, ListAppender listAppender) { + listAppender.stop(); + new LogbackHelper().getRootContext().getLogger(loggerClass) + .detachAppender(listAppender); + } +} diff --git a/server/sonar-main/src/test/java/org/sonar/application/process/EsProcessMonitorTest.java b/server/sonar-main/src/test/java/org/sonar/application/process/EsProcessMonitorTest.java index d0ef83444fc..7af20a08cc1 100644 --- a/server/sonar-main/src/test/java/org/sonar/application/process/EsProcessMonitorTest.java +++ b/server/sonar-main/src/test/java/org/sonar/application/process/EsProcessMonitorTest.java @@ -35,7 +35,7 @@ import org.elasticsearch.discovery.MasterNotDiscoveredException; import org.junit.Test; import org.slf4j.LoggerFactory; import org.sonar.process.ProcessId; -import org.sonar.process.command.EsCommand; +import org.sonar.application.command.EsCommand; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; diff --git a/server/sonar-main/src/test/java/org/sonar/application/process/ProcessLauncherImplTest.java b/server/sonar-main/src/test/java/org/sonar/application/process/ProcessLauncherImplTest.java index a0a4bba391d..36a288c1f02 100644 --- a/server/sonar-main/src/test/java/org/sonar/application/process/ProcessLauncherImplTest.java +++ b/server/sonar-main/src/test/java/org/sonar/application/process/ProcessLauncherImplTest.java @@ -31,8 +31,8 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.rules.TemporaryFolder; import org.sonar.process.ProcessId; -import org.sonar.process.command.JavaCommand; -import org.sonar.process.jmvoptions.JvmOptions; +import org.sonar.application.command.JavaCommand; +import org.sonar.application.command.JvmOptions; import org.sonar.process.sharedmemoryfile.AllProcessesCommands; import static org.assertj.core.api.Assertions.assertThat; |