]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-13078 enable configuring the server by environment variables
authorMichal Duda <michal.duda@sonarsource.com>
Wed, 12 Feb 2020 14:54:09 +0000 (15:54 +0100)
committerSonarTech <sonartech@sonarsource.com>
Sat, 22 Feb 2020 19:46:09 +0000 (20:46 +0100)
12 files changed:
server/sonar-main/src/main/java/org/sonar/application/config/AppSettingsLoaderImpl.java
server/sonar-main/src/test/java/org/sonar/application/config/AppSettingsLoaderImplTest.java
server/sonar-main/src/test/java/org/sonar/application/es/EsSettingsTest.java
server/sonar-process/src/main/java/org/sonar/process/ProcessProperties.java
server/sonar-server-common/src/main/java/org/sonar/server/setting/ThreadLocalSettings.java
server/sonar-server-common/src/test/java/org/sonar/server/setting/ThreadLocalSettingsTest.java
sonar-application/src/main/assembly/conf/sonar.properties
sonar-application/src/main/java/org/sonar/application/App.java
sonar-core/src/main/java/org/sonar/core/config/CorePropertyDefinitions.java
sonar-core/src/main/java/org/sonar/core/util/SettingFormatter.java [new file with mode: 0644]
sonar-core/src/test/java/org/sonar/core/config/CorePropertyDefinitionsTest.java
sonar-core/src/test/java/org/sonar/core/util/SettingFormatterTest.java [new file with mode: 0644]

index 5362f8e8818010d29b6581049ad2210e546cff69..3ba85f8e60668f8868c86faf18617bd88f80d0c6 100644 (file)
@@ -26,30 +26,40 @@ import java.io.InputStreamReader;
 import java.io.Reader;
 import java.net.URISyntaxException;
 import java.util.Arrays;
+import java.util.Optional;
 import java.util.Properties;
+import java.util.Set;
 import java.util.function.Consumer;
+import java.util.stream.Collectors;
 import org.slf4j.LoggerFactory;
 import org.sonar.core.extension.ServiceLoaderWrapper;
 import org.sonar.process.ConfigurationUtils;
 import org.sonar.process.NetworkUtilsImpl;
 import org.sonar.process.ProcessProperties;
 import org.sonar.process.Props;
+import org.sonar.process.System2;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Optional.ofNullable;
+import static org.sonar.core.util.SettingFormatter.fromJavaPropertyToEnvVariable;
 import static org.sonar.process.ProcessProperties.Property.PATH_HOME;
 
 public class AppSettingsLoaderImpl implements AppSettingsLoader {
 
+  private final System2 system;
   private final File homeDir;
   private final String[] cliArguments;
   private final Consumer<Props>[] consumers;
   private final ServiceLoaderWrapper serviceLoaderWrapper;
 
-  public AppSettingsLoaderImpl(String[] cliArguments, ServiceLoaderWrapper serviceLoaderWrapper) {
-    this(cliArguments, detectHomeDir(), serviceLoaderWrapper, new FileSystemSettings(), new JdbcSettings(), new ClusterSettings(NetworkUtilsImpl.INSTANCE));
+  public AppSettingsLoaderImpl(System2 system, String[] cliArguments, ServiceLoaderWrapper serviceLoaderWrapper) {
+    this(system, cliArguments, detectHomeDir(), serviceLoaderWrapper, new FileSystemSettings(), new JdbcSettings(),
+      new ClusterSettings(NetworkUtilsImpl.INSTANCE));
   }
 
-  AppSettingsLoaderImpl(String[] cliArguments, File homeDir, ServiceLoaderWrapper serviceLoaderWrapper, Consumer<Props>... consumers) {
+  @SafeVarargs
+  AppSettingsLoaderImpl(System2 system, String[] cliArguments, File homeDir, ServiceLoaderWrapper serviceLoaderWrapper, Consumer<Props>... consumers) {
+    this.system = system;
     this.cliArguments = cliArguments;
     this.homeDir = homeDir;
     this.serviceLoaderWrapper = serviceLoaderWrapper;
@@ -63,9 +73,10 @@ public class AppSettingsLoaderImpl implements AppSettingsLoader {
   @Override
   public AppSettings load() {
     Properties p = loadPropertiesFile(homeDir);
+    fetchSettingsFromEnvironment(system, p);
     p.putAll(CommandLineParser.parseArguments(cliArguments));
     p.setProperty(PATH_HOME.getKey(), homeDir.getAbsolutePath());
-    p = ConfigurationUtils.interpolateVariables(p, System.getenv());
+    p = ConfigurationUtils.interpolateVariables(p, system.getenv());
 
     // the difference between Properties and Props is that the latter
     // supports decryption of values, so it must be used when values
@@ -77,6 +88,17 @@ public class AppSettingsLoaderImpl implements AppSettingsLoader {
     return new AppSettingsImpl(props);
   }
 
+  private static void fetchSettingsFromEnvironment(System2 system, Properties properties) {
+    Set<String> possibleSettings = Arrays.stream(ProcessProperties.Property.values()).map(ProcessProperties.Property::getKey)
+      .collect(Collectors.toSet());
+    possibleSettings.addAll(properties.stringPropertyNames());
+    possibleSettings.forEach(key -> {
+      String environmentVarName = fromJavaPropertyToEnvVariable(key);
+      Optional<String> envVarValue = ofNullable(system.getenv(environmentVarName));
+      envVarValue.ifPresent(value -> properties.put(key, value));
+    });
+  }
+
   private static File detectHomeDir() {
     try {
       File appJar = new File(Class.forName("org.sonar.application.App").getProtectionDomain().getCodeSource().getLocation().toURI());
index b4c01e369309035cabe6ce95474512ad833607bd..988107a61501d45ef5e54ba3fb06f86931c24a1a 100644 (file)
  */
 package org.sonar.application.config;
 
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import java.io.File;
+import java.io.IOException;
 import org.apache.commons.io.FileUtils;
 import org.junit.Before;
 import org.junit.Rule;
@@ -28,7 +30,9 @@ import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.junit.rules.TemporaryFolder;
 import org.sonar.core.extension.ServiceLoaderWrapper;
+import org.sonar.process.System2;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.data.MapEntry.entry;
 import static org.mockito.Mockito.mock;
@@ -42,6 +46,7 @@ public class AppSettingsLoaderImplTest {
   public TemporaryFolder temp = new TemporaryFolder();
 
   private ServiceLoaderWrapper serviceLoaderWrapper = mock(ServiceLoaderWrapper.class);
+  private System2 system = mock(System2.class);
 
   @Before
   public void setup() {
@@ -52,20 +57,42 @@ public class AppSettingsLoaderImplTest {
   public void load_properties_from_file() throws Exception {
     File homeDir = temp.newFolder();
     File propsFile = new File(homeDir, "conf/sonar.properties");
-    FileUtils.write(propsFile, "foo=bar");
+    FileUtils.write(propsFile, "foo=bar", UTF_8);
 
-    AppSettingsLoaderImpl underTest = new AppSettingsLoaderImpl(new String[0], homeDir, serviceLoaderWrapper);
+    AppSettingsLoaderImpl underTest = new AppSettingsLoaderImpl(system, new String[0], homeDir, serviceLoaderWrapper);
     AppSettings settings = underTest.load();
 
     assertThat(settings.getProps().rawProperties()).contains(entry("foo", "bar"));
   }
 
+  @Test
+  public void load_properties_from_env() throws Exception {
+    when(system.getenv()).thenReturn(ImmutableMap.of(
+      "SONAR_DASHED_PROPERTY", "2",
+      "SONAR_JDBC_URL", "some_jdbc_url",
+      "SONAR_EMBEDDEDDATABASE_PORT", "8765"));
+    when(system.getenv("SONAR_DASHED_PROPERTY")).thenReturn("2");
+    when(system.getenv("SONAR_JDBC_URL")).thenReturn("some_jdbc_url");
+    when(system.getenv("SONAR_EMBEDDEDDATABASE_PORT")).thenReturn("8765");
+    File homeDir = temp.newFolder();
+    File propsFile = new File(homeDir, "conf/sonar.properties");
+    FileUtils.write(propsFile, "sonar.dashed-property=1", UTF_8);
+    AppSettingsLoaderImpl underTest = new AppSettingsLoaderImpl(system, new String[0], homeDir, serviceLoaderWrapper);
+
+    AppSettings settings = underTest.load();
+
+    assertThat(settings.getProps().rawProperties()).contains(
+      entry("sonar.dashed-property", "2"),
+      entry("sonar.jdbc.url", "some_jdbc_url"),
+      entry("sonar.embeddedDatabase.port", "8765"));
+  }
+
   @Test
   public void throws_ISE_if_file_fails_to_be_loaded() throws Exception {
     File homeDir = temp.newFolder();
     File propsFileAsDir = new File(homeDir, "conf/sonar.properties");
     FileUtils.forceMkdir(propsFileAsDir);
-    AppSettingsLoaderImpl underTest = new AppSettingsLoaderImpl(new String[0], homeDir, serviceLoaderWrapper);
+    AppSettingsLoaderImpl underTest = new AppSettingsLoaderImpl(system, new String[0], homeDir, serviceLoaderWrapper);
 
     expectedException.expect(IllegalStateException.class);
     expectedException.expectMessage("Cannot open file " + propsFileAsDir.getAbsolutePath());
@@ -77,7 +104,7 @@ public class AppSettingsLoaderImplTest {
   public void file_is_not_loaded_if_it_does_not_exist() throws Exception {
     File homeDir = temp.newFolder();
 
-    AppSettingsLoaderImpl underTest = new AppSettingsLoaderImpl(new String[0], homeDir, serviceLoaderWrapper);
+    AppSettingsLoaderImpl underTest = new AppSettingsLoaderImpl(system, new String[0], homeDir, serviceLoaderWrapper);
     AppSettings settings = underTest.load();
 
     // no failure, file is ignored
@@ -88,7 +115,7 @@ public class AppSettingsLoaderImplTest {
   public void command_line_arguments_are_included_to_settings() throws Exception {
     File homeDir = temp.newFolder();
 
-    AppSettingsLoaderImpl underTest = new AppSettingsLoaderImpl(new String[] {"-Dsonar.foo=bar", "-Dhello=world"}, homeDir, serviceLoaderWrapper);
+    AppSettingsLoaderImpl underTest = new AppSettingsLoaderImpl(system, new String[] {"-Dsonar.foo=bar", "-Dhello=world"}, homeDir, serviceLoaderWrapper);
     AppSettings settings = underTest.load();
 
     assertThat(settings.getProps().rawProperties())
@@ -97,20 +124,47 @@ public class AppSettingsLoaderImplTest {
   }
 
   @Test
-  public void command_line_arguments_make_precedence_over_properties_files() throws Exception {
+  public void command_line_arguments_take_precedence_over_properties_files() throws IOException {
     File homeDir = temp.newFolder();
     File propsFile = new File(homeDir, "conf/sonar.properties");
-    FileUtils.write(propsFile, "sonar.foo=file");
+    FileUtils.write(propsFile, "sonar.foo=file", UTF_8);
 
-    AppSettingsLoaderImpl underTest = new AppSettingsLoaderImpl(new String[] {"-Dsonar.foo=cli"}, homeDir, serviceLoaderWrapper);
+    AppSettingsLoaderImpl underTest = new AppSettingsLoaderImpl(system, new String[] {"-Dsonar.foo=cli"}, homeDir, serviceLoaderWrapper);
     AppSettings settings = underTest.load();
 
     assertThat(settings.getProps().rawProperties()).contains(entry("sonar.foo", "cli"));
   }
 
   @Test
-  public void detectHomeDir_returns_existing_dir() {
-    assertThat(new AppSettingsLoaderImpl(new String[0], serviceLoaderWrapper).getHomeDir()).exists().isDirectory();
+  public void env_vars_take_precedence_over_properties_file() throws Exception {
+    when(system.getenv()).thenReturn(ImmutableMap.of("SONAR_CUSTOMPROP", "11"));
+    when(system.getenv("SONAR_CUSTOMPROP")).thenReturn("11");
+    File homeDir = temp.newFolder();
+    File propsFile = new File(homeDir, "conf/sonar.properties");
+    FileUtils.write(propsFile, "sonar.customProp=10", UTF_8);
+
+    AppSettingsLoaderImpl underTest = new AppSettingsLoaderImpl(system, new String[0], homeDir, serviceLoaderWrapper);
+    AppSettings settings = underTest.load();
 
+    assertThat(settings.getProps().rawProperties()).contains(entry("sonar.customProp", "11"));
+  }
+
+  @Test
+  public void command_line_arguments_take_precedence_over_env_vars() throws Exception {
+    when(system.getenv()).thenReturn(ImmutableMap.of("SONAR_CUSTOMPROP", "11"));
+    when(system.getenv("SONAR_CUSTOMPROP")).thenReturn("11");
+    File homeDir = temp.newFolder();
+    File propsFile = new File(homeDir, "conf/sonar.properties");
+    FileUtils.write(propsFile, "sonar.customProp=10", UTF_8);
+
+    AppSettingsLoaderImpl underTest = new AppSettingsLoaderImpl(system, new String[] {"-Dsonar.customProp=9"}, homeDir, serviceLoaderWrapper);
+    AppSettings settings = underTest.load();
+
+    assertThat(settings.getProps().rawProperties()).contains(entry("sonar.customProp", "9"));
+  }
+
+  @Test
+  public void detectHomeDir_returns_existing_dir() {
+    assertThat(new AppSettingsLoaderImpl(system, new String[0], serviceLoaderWrapper).getHomeDir()).exists().isDirectory();
   }
 }
index ae166f57741b21d9452525670d2e48c57cd335bb..12c8d55db7ea78924d32d4e33e5dbca576336e5d 100644 (file)
@@ -66,6 +66,8 @@ public class EsSettingsTest {
   public ExpectedException expectedException = ExpectedException.none();
   private ListAppender listAppender;
 
+  private System2 system = mock(System2.class);
+
   @After
   public void tearDown() {
     if (listAppender != null) {
@@ -77,8 +79,7 @@ public class EsSettingsTest {
   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 EsInstallation(props), system2);
+    new EsSettings(props, new EsInstallation(props), system);
 
     assertThat(listAppender.getLogs()).isEmpty();
   }
@@ -87,9 +88,8 @@ public class EsSettingsTest {
   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 EsInstallation(props), system2);
+    when(system.getenv("ES_JVM_OPTIONS")).thenReturn("  ");
+    new EsSettings(props, new EsInstallation(props), system);
 
     assertThat(listAppender.getLogs()).isEmpty();
   }
@@ -98,9 +98,8 @@ public class EsSettingsTest {
   public void constructor_logs_warning_if_env_variable_ES_JVM_OPTIONS_is_set_and_non_empty() {
     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 EsInstallation(props), system2);
+    when(system.getenv("ES_JVM_OPTIONS")).thenReturn(randomAlphanumeric(2));
+    new EsSettings(props, new EsInstallation(props), system);
 
     assertThat(listAppender.getLogs())
       .extracting(ILoggingEvent::getMessage)
@@ -130,7 +129,7 @@ public class EsSettingsTest {
     props.set(PATH_LOGS.getKey(), temp.newFolder().getAbsolutePath());
     props.set(CLUSTER_NAME.getKey(), "sonarqube");
 
-    EsSettings esSettings = new EsSettings(props, new EsInstallation(props), System2.INSTANCE);
+    EsSettings esSettings = new EsSettings(props, new EsInstallation(props), system);
 
     Map<String, String> generated = esSettings.build();
     assertThat(generated.get("transport.tcp.port")).isEqualTo("1234");
@@ -169,7 +168,7 @@ public class EsSettingsTest {
     props.set(Property.CLUSTER_ENABLED.getKey(), "true");
     props.set(CLUSTER_NODE_NAME.getKey(), "node-1");
 
-    EsSettings esSettings = new EsSettings(props, new EsInstallation(props), System2.INSTANCE);
+    EsSettings esSettings = new EsSettings(props, new EsInstallation(props), system);
 
     Map<String, String> generated = esSettings.build();
     assertThat(generated.get("cluster.name")).isEqualTo("sonarqube-1");
@@ -188,7 +187,7 @@ public class EsSettingsTest {
     props.set(PATH_DATA.getKey(), temp.newFolder().getAbsolutePath());
     props.set(PATH_TEMP.getKey(), temp.newFolder().getAbsolutePath());
     props.set(PATH_LOGS.getKey(), temp.newFolder().getAbsolutePath());
-    EsSettings esSettings = new EsSettings(props, new EsInstallation(props), System2.INSTANCE);
+    EsSettings esSettings = new EsSettings(props, new EsInstallation(props), system);
     Map<String, String> generated = esSettings.build();
     assertThat(generated.get("node.name")).startsWith("sonarqube-");
   }
@@ -205,7 +204,7 @@ public class EsSettingsTest {
     props.set(PATH_DATA.getKey(), temp.newFolder().getAbsolutePath());
     props.set(PATH_TEMP.getKey(), temp.newFolder().getAbsolutePath());
     props.set(PATH_LOGS.getKey(), temp.newFolder().getAbsolutePath());
-    EsSettings esSettings = new EsSettings(props, new EsInstallation(props), System2.INSTANCE);
+    EsSettings esSettings = new EsSettings(props, new EsInstallation(props), system);
     Map<String, String> generated = esSettings.build();
     assertThat(generated.get("node.name")).isEqualTo("sonarqube");
   }
@@ -223,7 +222,7 @@ public class EsSettingsTest {
     File data = new File(foo, "data");
     when(mockedEsInstallation.getDataDirectory()).thenReturn(data);
 
-    EsSettings underTest = new EsSettings(minProps(new Random().nextBoolean()), mockedEsInstallation, System2.INSTANCE);
+    EsSettings underTest = new EsSettings(minProps(new Random().nextBoolean()), mockedEsInstallation, system);
 
     Map<String, String> generated = underTest.build();
     assertThat(generated.get("path.data")).isEqualTo(data.getPath());
@@ -235,7 +234,7 @@ public class EsSettingsTest {
   public void set_discovery_settings_if_cluster_is_enabled() throws Exception {
     Props props = minProps(CLUSTER_ENABLED);
     props.set(CLUSTER_SEARCH_HOSTS.getKey(), "1.2.3.4:9000,1.2.3.5:8080");
-    Map<String, String> settings = new EsSettings(props, new EsInstallation(props), System2.INSTANCE).build();
+    Map<String, String> settings = new EsSettings(props, new EsInstallation(props), system).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");
@@ -247,7 +246,7 @@ public class EsSettingsTest {
     Props props = minProps(CLUSTER_ENABLED);
     props.set(SEARCH_MINIMUM_MASTER_NODES.getKey(), "ꝱꝲꝳପ");
 
-    EsSettings underTest = new EsSettings(props, new EsInstallation(props), System2.INSTANCE);
+    EsSettings underTest = new EsSettings(props, new EsInstallation(props), system);
 
     expectedException.expect(IllegalStateException.class);
     expectedException.expectMessage("Value of property sonar.search.minimumMasterNodes is not an integer:");
@@ -258,7 +257,7 @@ public class EsSettingsTest {
   public void cluster_is_enabled_with_defined_minimum_master_nodes() throws Exception {
     Props props = minProps(CLUSTER_ENABLED);
     props.set(SEARCH_MINIMUM_MASTER_NODES.getKey(), "5");
-    Map<String, String> settings = new EsSettings(props, new EsInstallation(props), System2.INSTANCE).build();
+    Map<String, String> settings = new EsSettings(props, new EsInstallation(props), system).build();
 
     assertThat(settings.get("discovery.zen.minimum_master_nodes")).isEqualTo("5");
   }
@@ -267,7 +266,7 @@ public class EsSettingsTest {
   public void cluster_is_enabled_with_defined_initialTimeout() throws Exception {
     Props props = minProps(CLUSTER_ENABLED);
     props.set(SEARCH_INITIAL_STATE_TIMEOUT.getKey(), "10s");
-    Map<String, String> settings = new EsSettings(props, new EsInstallation(props), System2.INSTANCE).build();
+    Map<String, String> settings = new EsSettings(props, new EsInstallation(props), system).build();
 
     assertThat(settings.get("discovery.initial_state_timeout")).isEqualTo("10s");
   }
@@ -276,7 +275,7 @@ public class EsSettingsTest {
   public void in_standalone_initialTimeout_is_not_overridable() throws Exception {
     Props props = minProps(CLUSTER_DISABLED);
     props.set(SEARCH_INITIAL_STATE_TIMEOUT.getKey(), "10s");
-    Map<String, String> settings = new EsSettings(props, new EsInstallation(props), System2.INSTANCE).build();
+    Map<String, String> settings = new EsSettings(props, new EsInstallation(props), system).build();
 
     assertThat(settings.get("discovery.initial_state_timeout")).isEqualTo("30s");
   }
@@ -285,7 +284,7 @@ public class EsSettingsTest {
   public void in_standalone_minimumMasterNodes_is_not_overridable() throws Exception {
     Props props = minProps(CLUSTER_DISABLED);
     props.set(SEARCH_MINIMUM_MASTER_NODES.getKey(), "5");
-    Map<String, String> settings = new EsSettings(props, new EsInstallation(props), System2.INSTANCE).build();
+    Map<String, String> settings = new EsSettings(props, new EsInstallation(props), system).build();
 
     assertThat(settings.get("discovery.zen.minimum_master_nodes")).isEqualTo("1");
   }
@@ -294,7 +293,7 @@ public class EsSettingsTest {
   public void enable_http_connector() throws Exception {
     Props props = minProps(CLUSTER_DISABLED);
     props.set(SEARCH_HTTP_PORT.getKey(), "9010");
-    Map<String, String> settings = new EsSettings(props, new EsInstallation(props), System2.INSTANCE).build();
+    Map<String, String> settings = new EsSettings(props, new EsInstallation(props), system).build();
 
     assertThat(settings.get("http.port")).isEqualTo("9010");
     assertThat(settings.get("http.host")).isEqualTo("127.0.0.1");
@@ -306,7 +305,7 @@ public class EsSettingsTest {
     Props props = minProps(CLUSTER_DISABLED);
     props.set(SEARCH_HTTP_PORT.getKey(), "9010");
     props.set(SEARCH_HOST.getKey(), "127.0.0.2");
-    Map<String, String> settings = new EsSettings(props, new EsInstallation(props), System2.INSTANCE).build();
+    Map<String, String> settings = new EsSettings(props, new EsInstallation(props), system).build();
 
     assertThat(settings.get("http.port")).isEqualTo("9010");
     assertThat(settings.get("http.host")).isEqualTo("127.0.0.2");
@@ -316,7 +315,7 @@ public class EsSettingsTest {
   @Test
   public void enable_seccomp_filter_by_default() throws Exception {
     Props props = minProps(CLUSTER_DISABLED);
-    Map<String, String> settings = new EsSettings(props, new EsInstallation(props), System2.INSTANCE).build();
+    Map<String, String> settings = new EsSettings(props, new EsInstallation(props), system).build();
 
     assertThat(settings.get("bootstrap.system_call_filter")).isNull();
   }
@@ -325,7 +324,7 @@ public class EsSettingsTest {
   public void disable_seccomp_filter_if_configured_in_search_additional_props() throws Exception {
     Props props = minProps(CLUSTER_DISABLED);
     props.set("sonar.search.javaAdditionalOpts", "-Xmx1G -Dbootstrap.system_call_filter=false -Dfoo=bar");
-    Map<String, String> settings = new EsSettings(props, new EsInstallation(props), System2.INSTANCE).build();
+    Map<String, String> settings = new EsSettings(props, new EsInstallation(props), system).build();
 
     assertThat(settings.get("bootstrap.system_call_filter")).isEqualTo("false");
   }
@@ -334,7 +333,7 @@ public class EsSettingsTest {
   public void disable_mmap_if_configured_in_search_additional_props() throws Exception {
     Props props = minProps(CLUSTER_DISABLED);
     props.set("sonar.search.javaAdditionalOpts", "-Dnode.store.allow_mmapfs=false");
-    Map<String, String> settings = new EsSettings(props, new EsInstallation(props), System2.INSTANCE).build();
+    Map<String, String> settings = new EsSettings(props, new EsInstallation(props), system).build();
 
     assertThat(settings.get("node.store.allow_mmapfs")).isEqualTo("false");
   }
index fa9a8657aa565148bd1791427f92f653043fc1ab..f643c4a744c3088378fe31c78b106c72dde5bcc3 100644 (file)
@@ -62,6 +62,11 @@ public class ProcessProperties {
     PATH_TEMP("sonar.path.temp", "temp"),
     PATH_WEB("sonar.path.web", "web"),
 
+    LOG_LEVEL_APP("sonar.log.level.app"),
+    LOG_LEVEL_WEB("sonar.log.level.web"),
+    LOG_LEVEL_CE("sonar.log.level.ce"),
+    LOG_LEVEL_ES("sonar.log.level.es"),
+
     SEARCH_HOST("sonar.search.host", InetAddress.getLoopbackAddress().getHostAddress()),
     SEARCH_PORT("sonar.search.port", "9001"),
     SEARCH_HTTP_PORT("sonar.search.httpPort"),
index 037ccc670a3258ace6458c8a0b4add32a3a34fe2..f2cf05f92d6b06dc79d439b12940dd19fb603542 100644 (file)
 package org.sonar.server.setting;
 
 import com.google.common.annotations.VisibleForTesting;
+import java.util.AbstractMap;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Properties;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import org.apache.ibatis.exceptions.PersistenceException;
 import org.sonar.api.CoreProperties;
 import org.sonar.api.ce.ComputeEngineSide;
 import org.sonar.api.config.Encryption;
+import org.sonar.api.config.PropertyDefinition;
 import org.sonar.api.config.PropertyDefinitions;
 import org.sonar.api.config.Settings;
 import org.sonar.api.server.ServerSide;
-import org.sonar.api.utils.log.Logger;
-import org.sonar.api.utils.log.Loggers;
+import org.sonar.api.utils.System2;
+import org.sonar.core.util.SettingFormatter;
 
-import static java.lang.String.format;
 import static java.util.Collections.unmodifiableMap;
 import static java.util.Objects.requireNonNull;
 
@@ -57,27 +60,29 @@ import static java.util.Objects.requireNonNull;
 @ComputeEngineSide
 @ServerSide
 public class ThreadLocalSettings extends Settings {
-  private static final Logger LOG = Loggers.get(ThreadLocalSettings.class);
-
-  private final Properties overwrittenSystemProps = new Properties();
   private final Properties systemProps = new Properties();
+  private final Properties corePropsFromEnvVariables = new Properties();
   private static final ThreadLocal<Map<String, String>> CACHE = new ThreadLocal<>();
   private Map<String, String> getPropertyDbFailureCache = Collections.emptyMap();
   private Map<String, String> getPropertiesDbFailureCache = Collections.emptyMap();
   private SettingLoader settingLoader;
+  private System2 system2;
 
-  public ThreadLocalSettings(PropertyDefinitions definitions, Properties props) {
-    this(definitions, props, new NopSettingLoader());
+  public ThreadLocalSettings(System2 system2, PropertyDefinitions definitions, Properties props) {
+    this(system2, definitions, props, new NopSettingLoader());
   }
 
   @VisibleForTesting
-  ThreadLocalSettings(PropertyDefinitions definitions, Properties props, SettingLoader settingLoader) {
+  ThreadLocalSettings(System2 system2, PropertyDefinitions definitions, Properties props, SettingLoader settingLoader) {
     super(definitions, new Encryption(null));
+    this.system2 = system2;
     this.settingLoader = settingLoader;
+
+    resolveCorePropertiesFromEnvironment();
     props.forEach((k, v) -> systemProps.put(k, v == null ? null : v.toString().trim()));
 
     // TODO something wrong about lifecycle here. It could be improved
-    getEncryption().setPathToSecretKey(props.getProperty(CoreProperties.ENCRYPTION_SECRET_KEY_PATH));
+    getEncryption().setPathToSecretKey(get(CoreProperties.ENCRYPTION_SECRET_KEY_PATH).orElse(null));
   }
 
   @VisibleForTesting
@@ -92,17 +97,17 @@ public class ThreadLocalSettings extends Settings {
   @Override
   protected Optional<String> get(String key) {
     // search for the first value available in
-    // 1. overwritten system properties
-    // 2. system properties
+    // 1. system properties
+    // 2. core property from environment variable
     // 3. thread local cache (if enabled)
     // 4. db
 
-    String value =  overwrittenSystemProps.getProperty(key);
+    String value = systemProps.getProperty(key);
     if (value != null) {
       return Optional.of(value);
     }
 
-    value = systemProps.getProperty(key);
+    value = corePropsFromEnvVariables.getProperty(key);
     if (value != null) {
       return Optional.of(value);
     }
@@ -135,15 +140,6 @@ public class ThreadLocalSettings extends Settings {
     }
   }
 
-  public void setSystemProperty(String key, String value) {
-    checkKeyAndValue(key, value);
-    String systemValue = systemProps.getProperty(key);
-    if (LOG.isDebugEnabled() && systemValue != null && !value.equals(systemValue)) {
-      LOG.debug(format("System property '%s' with value '%s' overwritten with value '%s'", key, systemValue, value));
-    }
-    overwrittenSystemProps.put(key, value.trim());
-  }
-
   @Override
   protected void set(String key, String value) {
     checkKeyAndValue(key, value);
@@ -187,6 +183,7 @@ public class ThreadLocalSettings extends Settings {
   public Map<String, String> getProperties() {
     Map<String, String> result = new HashMap<>();
     loadAll(result);
+    corePropsFromEnvVariables.forEach((k, v) -> result.put((String) k, (String) v));
     systemProps.forEach((key, value) -> result.put((String) key, (String) value));
     return unmodifiableMap(result);
   }
@@ -200,4 +197,20 @@ public class ThreadLocalSettings extends Settings {
       appendTo.putAll(getPropertiesDbFailureCache);
     }
   }
+
+  private void resolveCorePropertiesFromEnvironment() {
+    corePropsFromEnvVariables.putAll(this.getDefinitions().getAll()
+      .stream()
+      .map(PropertyDefinition::key)
+      .flatMap(p -> {
+        String envVar = SettingFormatter.fromJavaPropertyToEnvVariable(p);
+        String envVarValue = system2.envVariable(envVar);
+        if (envVarValue != null) {
+          return Stream.of(new AbstractMap.SimpleEntry<>(p, envVarValue));
+        } else {
+          return Stream.empty();
+        }
+      })
+      .collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue)));
+  }
 }
index ddcfef1682768b15f41a3fe977cf1c0fa36cc3e9..90624314c4a1c5edaa20235f283bb7aa1cd4ae7a 100644 (file)
@@ -34,6 +34,8 @@ import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.junit.rules.TemporaryFolder;
 import org.sonar.api.config.PropertyDefinitions;
+import org.sonar.api.utils.System2;
+import org.sonar.core.config.CorePropertyDefinitions;
 
 import static java.util.Collections.emptyMap;
 import static java.util.Collections.unmodifiableMap;
@@ -44,6 +46,7 @@ import static org.assertj.core.data.MapEntry.entry;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 public class ThreadLocalSettingsTest {
 
@@ -56,6 +59,7 @@ public class ThreadLocalSettingsTest {
 
   private MapSettingLoader dbSettingLoader = new MapSettingLoader();
   private ThreadLocalSettings underTest = null;
+  private System2 system = mock(System2.class);
 
   @After
   public void tearDown() {
@@ -141,7 +145,7 @@ public class ThreadLocalSettingsTest {
   private ThreadLocalSettings create(Map<String, String> systemProps) {
     Properties p = new Properties();
     p.putAll(systemProps);
-    return new ThreadLocalSettings(new PropertyDefinitions(), p, dbSettingLoader);
+    return new ThreadLocalSettings(system, new PropertyDefinitions(CorePropertyDefinitions.all()), p, dbSettingLoader);
   }
 
   @Test
@@ -153,6 +157,16 @@ public class ThreadLocalSettingsTest {
     assertThat(underTest.getProperties()).containsOnly(entry("foo", "1"), entry("bar", "2"));
   }
 
+  @Test
+  public void load_core_properties_from_environment() {
+    when(system.envVariable("SONAR_FORCEAUTHENTICATION")).thenReturn("true");
+    underTest = create(ImmutableMap.of());
+
+    assertThat(underTest.get("sonar.forceAuthentication")).hasValue("true");
+    assertThat(underTest.get("missing")).isNotPresent();
+    assertThat(underTest.getProperties()).containsOnly(entry("sonar.forceAuthentication", "true"));
+  }
+
   @Test
   public void database_properties_are_not_cached_by_default() {
     insertPropertyIntoDb("foo", "from db");
@@ -165,25 +179,6 @@ public class ThreadLocalSettingsTest {
     assertThat(underTest.get("foo")).isNotPresent();
   }
 
-  @Test
-  public void overwritten_system_settings_have_precedence_over_system_and_databse() {
-    underTest = create(ImmutableMap.of("foo", "from system"));
-
-    underTest.setSystemProperty("foo", "donut");
-
-    assertThat(underTest.get("foo")).hasValue("donut");
-  }
-
-  @Test
-  public void overwritten_system_settings_have_precedence_over_databse() {
-    insertPropertyIntoDb("foo", "from db");
-    underTest = create(Collections.emptyMap());
-
-    underTest.setSystemProperty("foo", "donut");
-
-    assertThat(underTest.get("foo")).hasValue("donut");
-  }
-
   @Test
   public void system_settings_have_precedence_over_database() {
     insertPropertyIntoDb("foo", "from db");
@@ -268,7 +263,7 @@ public class ThreadLocalSettingsTest {
 
   @Test
   public void change_setting_loader() {
-    underTest = new ThreadLocalSettings(new PropertyDefinitions(), new Properties());
+    underTest = new ThreadLocalSettings(system, new PropertyDefinitions(), new Properties());
 
     assertThat(underTest.getSettingLoader()).isNotNull();
 
@@ -291,7 +286,7 @@ public class ThreadLocalSettingsTest {
     SettingLoader settingLoaderMock = mock(SettingLoader.class);
     PersistenceException toBeThrown = new PersistenceException("Faking an error connecting to DB");
     doThrow(toBeThrown).when(settingLoaderMock).loadAll();
-    underTest = new ThreadLocalSettings(new PropertyDefinitions(), new Properties(), settingLoaderMock);
+    underTest = new ThreadLocalSettings(system, new PropertyDefinitions(), new Properties(), settingLoaderMock);
 
     assertThat(underTest.getProperties())
       .isEmpty();
@@ -302,7 +297,7 @@ public class ThreadLocalSettingsTest {
     SettingLoader settingLoaderMock = mock(SettingLoader.class);
     PersistenceException toBeThrown = new PersistenceException("Faking an error connecting to DB");
     doThrow(toBeThrown).when(settingLoaderMock).loadAll();
-    underTest = new ThreadLocalSettings(new PropertyDefinitions(), new Properties(), settingLoaderMock);
+    underTest = new ThreadLocalSettings(system, new PropertyDefinitions(), new Properties(), settingLoaderMock);
     underTest.load();
 
     assertThat(underTest.getProperties())
@@ -321,7 +316,7 @@ public class ThreadLocalSettingsTest {
       .doAnswer(invocationOnMock -> ImmutableMap.of(key, value2))
       .when(settingLoaderMock)
       .loadAll();
-    underTest = new ThreadLocalSettings(new PropertyDefinitions(), new Properties(), settingLoaderMock);
+    underTest = new ThreadLocalSettings(system, new PropertyDefinitions(), new Properties(), settingLoaderMock);
 
     underTest.load();
     assertThat(underTest.getProperties())
@@ -345,7 +340,7 @@ public class ThreadLocalSettingsTest {
     PersistenceException toBeThrown = new PersistenceException("Faking an error connecting to DB");
     String key = randomAlphanumeric(3);
     doThrow(toBeThrown).when(settingLoaderMock).load(key);
-    underTest = new ThreadLocalSettings(new PropertyDefinitions(), new Properties(), settingLoaderMock);
+    underTest = new ThreadLocalSettings(system, new PropertyDefinitions(), new Properties(), settingLoaderMock);
 
     assertThat(underTest.get(key)).isEmpty();
   }
@@ -356,7 +351,7 @@ public class ThreadLocalSettingsTest {
     PersistenceException toBeThrown = new PersistenceException("Faking an error connecting to DB");
     String key = randomAlphanumeric(3);
     doThrow(toBeThrown).when(settingLoaderMock).load(key);
-    underTest = new ThreadLocalSettings(new PropertyDefinitions(), new Properties(), settingLoaderMock);
+    underTest = new ThreadLocalSettings(system, new PropertyDefinitions(), new Properties(), settingLoaderMock);
     underTest.load();
 
     assertThat(underTest.get(key)).isEmpty();
index b7067153c6ae83d584e4d4cff530a4299e9ed76e..f26ac7012362c0130e8088db264f5a9568b102b2 100644 (file)
@@ -1,5 +1,7 @@
 # Property values can:
-# - reference an environment variable, for example sonar.jdbc.url= ${env:SONAR_JDBC_URL}
+# - be overridden by environment variables. The name of the corresponding environment variable is the
+#   upper-cased name of the property where all the dot ('.') and dash ('-') characters are replaced by
+#   underscores ('_'). For example, to override 'sonar.web.systemPasscode' use 'SONAR_WEB_SYSTEMPASSCODE'.
 # - be encrypted. See https://redirect.sonarsource.com/doc/settings-encryption.html
 
 #--------------------------------------------------------------------------------------------------
index 0260d25ca5b1a521d1dde44076e33b8d33183140..5ab3ae1214261f77a0b5b572fcd0fbe437a1213c 100644 (file)
@@ -42,12 +42,13 @@ public class App {
   private final JavaVersion javaVersion;
   private StopRequestWatcher stopRequestWatcher = null;
   private StopRequestWatcher hardStopRequestWatcher = null;
+
   public App(JavaVersion javaVersion) {
     this.javaVersion = javaVersion;
   }
 
   public void start(String[] cliArguments) {
-    AppSettingsLoader settingsLoader = new AppSettingsLoaderImpl(cliArguments, new ServiceLoaderWrapper());
+    AppSettingsLoader settingsLoader = new AppSettingsLoaderImpl(System2.INSTANCE, cliArguments, new ServiceLoaderWrapper());
     AppSettings settings = settingsLoader.load();
     // order is important - logging must be configured before any other components (AppFileSystem, ...)
     AppLogging logging = new AppLogging(settings);
index 8eccefd97b1ae907cc104ecf42749e7277366289..295cc2fdaf7754a896027240e4a4da10aa7bed81 100644 (file)
@@ -29,6 +29,7 @@ import org.sonar.api.resources.Qualifiers;
 
 import static java.util.Arrays.asList;
 import static org.sonar.api.PropertyType.BOOLEAN;
+import static org.sonar.api.PropertyType.STRING;
 
 public class CorePropertyDefinitions {
 
@@ -70,6 +71,12 @@ public class CorePropertyDefinitions {
         .category(CoreProperties.CATEGORY_GENERAL)
         .build(),
 
+      PropertyDefinition.builder(CoreProperties.ENCRYPTION_SECRET_KEY_PATH)
+        .name("Encryption secret key path")
+        .description("Path to a file that contains encryption secret key that is used to encrypting other settings.")
+        .type(STRING)
+        .hidden()
+        .build(),
       PropertyDefinition.builder("sonar.authenticator.downcase")
         .name("Downcase login")
         .description("Downcase login during user authentication, typically for Active Directory")
diff --git a/sonar-core/src/main/java/org/sonar/core/util/SettingFormatter.java b/sonar-core/src/main/java/org/sonar/core/util/SettingFormatter.java
new file mode 100644 (file)
index 0000000..7233a79
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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.core.util;
+
+import java.util.Locale;
+
+public final class SettingFormatter {
+  private SettingFormatter() {
+    // util class
+  }
+
+  public static String fromJavaPropertyToEnvVariable(String property) {
+    return property.toUpperCase(Locale.ENGLISH).replace('.', '_').replace('-', '_');
+  }
+}
index 50d281dc4941f75471441e0fad6f5a3b8707fd89..e03f5d7868dfb3dc3eae116389143564da15863f 100644 (file)
@@ -30,7 +30,7 @@ public class CorePropertyDefinitionsTest {
   @Test
   public void all() {
     List<PropertyDefinition> defs = CorePropertyDefinitions.all();
-    assertThat(defs).hasSize(50);
+    assertThat(defs).hasSize(51);
   }
 
   @Test
diff --git a/sonar-core/src/test/java/org/sonar/core/util/SettingFormatterTest.java b/sonar-core/src/test/java/org/sonar/core/util/SettingFormatterTest.java
new file mode 100644 (file)
index 0000000..d9aed76
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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.core.util;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class SettingFormatterTest {
+
+  @Test
+  public void fromJavaPropertyToEnvVariable() {
+    String output = SettingFormatter.fromJavaPropertyToEnvVariable("some.randomProperty-123.test");
+    assertThat(output).isEqualTo("SOME_RANDOMPROPERTY_123_TEST");
+  }
+}