From 7a68e525099ff79027c2aee162da3d05b92fd6aa Mon Sep 17 00:00:00 2001 From: Julien HENRY Date: Tue, 16 Apr 2024 18:20:26 +0200 Subject: [PATCH] SONAR-22086 Add support to configure scanner with env variables --- .../org/sonar/batch/bootstrapper/Batch.java | 2 + .../scanner/bootstrap/EnvironmentConfig.java | 99 ++++++++++ .../sonar/scanner/bootstrap/ScannerMain.java | 2 + .../bootstrap/EnvironmentConfigTest.java | 170 ++++++++++++++++++ 4 files changed, 273 insertions(+) create mode 100644 sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/EnvironmentConfig.java create mode 100644 sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/EnvironmentConfigTest.java diff --git a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/Batch.java b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/Batch.java index 75a6a1bdee8..19b90c180fa 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/Batch.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrapper/Batch.java @@ -26,6 +26,7 @@ import java.util.List; import java.util.Map; import javax.annotation.Nullable; import org.sonar.api.utils.MessageException; +import org.sonar.scanner.bootstrap.EnvironmentConfig; import org.sonar.scanner.bootstrap.SpringGlobalContainer; /** @@ -47,6 +48,7 @@ public final class Batch { } if (builder.globalProperties != null) { globalProperties.putAll(builder.globalProperties); + EnvironmentConfig.processEnvVariables(globalProperties); } if (builder.isEnableLoggingConfiguration()) { loggingConfig = new LoggingConfiguration(builder.environment).setProperties(globalProperties); diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/EnvironmentConfig.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/EnvironmentConfig.java new file mode 100644 index 00000000000..d26e5b46416 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/EnvironmentConfig.java @@ -0,0 +1,99 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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.scanner.bootstrap; + +import com.google.gson.Gson; +import java.util.Map; +import java.util.stream.Stream; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.LoggerFactory; + +public class EnvironmentConfig { + + private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(EnvironmentConfig.class); + + private static final String SONAR_SCANNER_JSON_PARAMS = "SONAR_SCANNER_JSON_PARAMS"; + private static final String SONARQUBE_SCANNER_PARAMS = "SONARQUBE_SCANNER_PARAMS"; + private static final String GENERIC_ENV_PREFIX = "SONAR_SCANNER_"; + + private EnvironmentConfig() { + // only static methods + } + + public static void processEnvVariables(Map inputProperties) { + processEnvVariables(inputProperties, System.getenv()); + } + + static void processEnvVariables(Map inputProperties, Map env) { + env.forEach((key, value) -> { + if (!key.equals(SONAR_SCANNER_JSON_PARAMS) && key.startsWith(GENERIC_ENV_PREFIX)) { + processEnvVariable(key, value, inputProperties); + } + }); + var jsonParams = env.get(SONAR_SCANNER_JSON_PARAMS); + var oldJsonParams = env.get(SONARQUBE_SCANNER_PARAMS); + if (jsonParams != null) { + if (oldJsonParams != null && !oldJsonParams.equals(jsonParams)) { + LOG.warn("Ignoring environment variable '{}' because '{}' is set", SONARQUBE_SCANNER_PARAMS, SONAR_SCANNER_JSON_PARAMS); + } + parseJsonPropertiesFromEnv(jsonParams, inputProperties, SONAR_SCANNER_JSON_PARAMS); + } else if (oldJsonParams != null) { + parseJsonPropertiesFromEnv(oldJsonParams, inputProperties, SONARQUBE_SCANNER_PARAMS); + } + } + + private static void parseJsonPropertiesFromEnv(String jsonParams, Map inputProperties, String envVariableName) { + try { + var jsonProperties = new Gson().>fromJson(jsonParams, Map.class); + if (jsonProperties != null) { + jsonProperties.forEach((key, value) -> { + if (inputProperties.containsKey(key)) { + if (!inputProperties.get(key).equals(value)) { + LOG.warn("Ignoring property '{}' from env variable '{}' because it is already defined", key, envVariableName); + } + } else { + inputProperties.put(key, value); + } + }); + } + } catch (Exception e) { + throw new IllegalArgumentException("Failed to parse JSON properties from environment variable '" + envVariableName + "'", e); + } + } + + private static void processEnvVariable(String key, String value, Map inputProperties) { + var suffix = key.substring(GENERIC_ENV_PREFIX.length()); + if (suffix.isEmpty()) { + return; + } + var toCamelCase = Stream.of(suffix.split("_")) + .map(String::toLowerCase) + .reduce((a, b) -> a + StringUtils.capitalize(b)).orElseThrow(); + var propKey = "sonar.scanner." + toCamelCase; + if (inputProperties.containsKey(propKey)) { + if (!inputProperties.get(propKey).equals(value)) { + LOG.warn("Ignoring environment variable '{}' because it is already defined in the properties", key); + } + } else { + inputProperties.put(propKey, value); + } + } + +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerMain.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerMain.java index 405f586d654..044b42fd620 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerMain.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerMain.java @@ -60,6 +60,8 @@ public class ScannerMain { var properties = parseInputProperties(in); + EnvironmentConfig.processEnvVariables(properties); + configureLogLevel(properties); runScannerEngine(properties); diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/EnvironmentConfigTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/EnvironmentConfigTest.java new file mode 100644 index 00000000000..26f62d4f6d0 --- /dev/null +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/EnvironmentConfigTest.java @@ -0,0 +1,170 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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.scanner.bootstrap; + +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.slf4j.event.Level; +import org.sonar.api.testfixtures.log.LogTesterJUnit5; + +import static java.util.Map.entry; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertThrows; + +class EnvironmentConfigTest { + + @RegisterExtension + private final LogTesterJUnit5 logTester = new LogTesterJUnit5(); + + @Test + void shouldProcessGenericEnvVariables() { + var inputProperties = new HashMap(); + EnvironmentConfig.processEnvVariables(inputProperties, + Map.of("SONAR_SCANNER", "ignored", + "SONAR_SCANNER_", "ignored as well", + "SONAR_SCANNER_FOO", "bar", + "SONAR_SCANNER_FOO_BAZ", "bar", + "SONAR_SCANNER_fuZz_bAz", "env vars are case insensitive")); + + assertThat(inputProperties).containsOnly( + entry("sonar.scanner.foo", "bar"), + entry("sonar.scanner.fooBaz", "bar"), + entry("sonar.scanner.fuzzBaz", "env vars are case insensitive")); + } + + @Test + void genericEnvVarShouldNotOverrideInputProperties() { + var inputProperties = new HashMap(Map.of("sonar.scanner.foo", "foo", "sonar.scanner.bar", "same value")); + EnvironmentConfig.processEnvVariables(inputProperties, + Map.of( + "SONAR_SCANNER_FOO", "should not override", + "SONAR_SCANNER_BAR", "same value", + "SONAR_SCANNER_BAZ", "baz")); + + assertThat(inputProperties).containsOnly( + entry("sonar.scanner.foo", "foo"), + entry("sonar.scanner.bar", "same value"), + entry("sonar.scanner.baz", "baz")); + + assertThat(logTester.logs(Level.WARN)).containsOnly("Ignoring environment variable 'SONAR_SCANNER_FOO' because it is already defined in the properties"); + } + + @Test + void shouldProcessJsonEnvVariables() { + var inputProperties = new HashMap(); + EnvironmentConfig.processEnvVariables(inputProperties, + Map.of("SONAR_SCANNER_JSON_PARAMS", + "{\"key1\":\"value1\", \"key2\":\"value2\"}")); + + assertThat(inputProperties).containsOnly( + entry("key1", "value1"), + entry("key2", "value2")); + } + + @Test + void ignoreEmptyValueForJsonEnv() { + var inputProperties = new HashMap(); + EnvironmentConfig.processEnvVariables(inputProperties, + Map.of("SONAR_SCANNER_JSON_PARAMS", "")); + + assertThat(inputProperties).isEmpty(); + } + + @Test + void throwIfInvalidFormat() { + var inputProperties = new HashMap(); + var env = Map.of("SONAR_SCANNER_JSON_PARAMS", "{garbage"); + var thrown = assertThrows(IllegalArgumentException.class, () -> EnvironmentConfig.processEnvVariables(inputProperties, env)); + + assertThat(thrown).hasMessage("Failed to parse JSON properties from environment variable 'SONAR_SCANNER_JSON_PARAMS'"); + } + + @Test + void jsonEnvVariablesShouldNotOverrideInputProperties() { + var inputProperties = new HashMap(Map.of("key1", "value1", "key3", "value3")); + EnvironmentConfig.processEnvVariables(inputProperties, + Map.of("SONAR_SCANNER_JSON_PARAMS", + "{\"key1\":\"should not override\", \"key2\":\"value2\"}")); + + assertThat(inputProperties).containsOnly( + entry("key1", "value1"), + entry("key2", "value2"), + entry("key3", "value3")); + + assertThat(logTester.logs(Level.WARN)).containsOnly("Ignoring property 'key1' from env variable 'SONAR_SCANNER_JSON_PARAMS' because it is already defined"); + } + + @Test + void jsonEnvVariablesShouldNotOverrideGenericEnv() { + var inputProperties = new HashMap(); + EnvironmentConfig.processEnvVariables(inputProperties, + Map.of("SONAR_SCANNER_FOO", "value1", + "SONAR_SCANNER_JSON_PARAMS", "{\"sonar.scanner.foo\":\"should not override\", \"key2\":\"value2\"}")); + + assertThat(inputProperties).containsOnly( + entry("sonar.scanner.foo", "value1"), + entry("key2", "value2")); + + assertThat(logTester.logs(Level.WARN)).containsOnly("Ignoring property 'sonar.scanner.foo' from env variable 'SONAR_SCANNER_JSON_PARAMS' because it is already defined"); + } + + @Test + void shouldProcessOldJsonEnvVariables() { + var inputProperties = new HashMap(); + EnvironmentConfig.processEnvVariables(inputProperties, + Map.of("SONARQUBE_SCANNER_PARAMS", + "{\"key1\":\"value1\", \"key2\":\"value2\"}")); + + assertThat(inputProperties).containsOnly( + entry("key1", "value1"), + entry("key2", "value2")); + } + + @Test + void oldJsonEnvVariablesIsIgnoredIfNewIsDefinedAndLogAWarning() { + var inputProperties = new HashMap(); + EnvironmentConfig.processEnvVariables(inputProperties, + Map.of("SONARQUBE_SCANNER_PARAMS", "{\"key1\":\"should not override\", \"key3\":\"value3\"}", + "SONAR_SCANNER_JSON_PARAMS", "{\"key1\":\"value1\", \"key2\":\"value2\"}")); + + assertThat(inputProperties).containsOnly( + entry("key1", "value1"), + entry("key2", "value2")); + + assertThat(logTester.logs(Level.WARN)).containsOnly("Ignoring environment variable 'SONARQUBE_SCANNER_PARAMS' because 'SONAR_SCANNER_JSON_PARAMS' is set"); + } + + @Test + void oldJsonEnvVariablesIsIgnoredIfNewIsDefinedButDontLogIfSameValue() { + var inputProperties = new HashMap(); + EnvironmentConfig.processEnvVariables(inputProperties, + Map.of("SONARQUBE_SCANNER_PARAMS", "{\"key1\":\"value1\", \"key2\":\"value2\"}", + "SONAR_SCANNER_JSON_PARAMS", "{\"key1\":\"value1\", \"key2\":\"value2\"}")); + + assertThat(inputProperties).containsOnly( + entry("key1", "value1"), + entry("key2", "value2")); + + assertThat(logTester.logs()).isEmpty(); + } + +} -- 2.39.5