Skip to content

Commit ca88ec5

Browse files
SCANPY-114 Parse the scanner properties from the environnement variables (#162)
1 parent 42b5c6f commit ca88ec5

File tree

6 files changed

+315
-10
lines changed

6 files changed

+315
-10
lines changed

src/pysonar_scanner/configuration/configuration_loader.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from pysonar_scanner.configuration.pyproject_toml import TomlConfigurationLoader
2424
from pysonar_scanner.configuration.properties import SONAR_TOKEN, SONAR_PROJECT_BASE_DIR, Key
2525
from pysonar_scanner.configuration.properties import PROPERTIES
26-
from pysonar_scanner.configuration import sonar_project_properties
26+
from pysonar_scanner.configuration import sonar_project_properties, environment_variables
2727

2828
from pysonar_scanner.exceptions import MissingKeyException
2929

@@ -51,6 +51,7 @@ def load() -> dict[Key, any]:
5151
resolved_properties.update(toml_properties.project_properties)
5252
resolved_properties.update(sonar_project_properties.load(base_dir))
5353
resolved_properties.update(toml_properties.sonar_properties)
54+
resolved_properties.update(environment_variables.load())
5455
resolved_properties.update(cli_properties)
5556
return resolved_properties
5657

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#
2+
# Sonar Scanner Python
3+
# Copyright (C) 2011-2024 SonarSource SA.
4+
# mailto:info AT sonarsource DOT com
5+
#
6+
# This program is free software; you can redistribute it and/or
7+
# modify it under the terms of the GNU Lesser General Public
8+
# License as published by the Free Software Foundation; either
9+
# version 3 of the License, or (at your option) any later version.
10+
# This program is distributed in the hope that it will be useful,
11+
#
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
# Lesser General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU Lesser General Public License
17+
# along with this program; if not, write to the Free Software Foundation,
18+
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
#
20+
import os
21+
from typing import Dict
22+
23+
from pysonar_scanner.configuration.properties import Key, PROPERTIES
24+
25+
26+
def load() -> Dict[Key, str]:
27+
"""
28+
Load configuration properties from environment variables.
29+
30+
Returns:
31+
Dictionary of property keys and their values extracted from environment variables.
32+
"""
33+
properties = {}
34+
# Extract properties from environment variables using the env_variable_name() method
35+
for prop in PROPERTIES:
36+
env_var_name = prop.env_variable_name()
37+
if env_var_name in os.environ:
38+
properties[prop.name] = os.environ[env_var_name]
39+
40+
return properties

src/pysonar_scanner/configuration/properties.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,21 @@ def python_name(self) -> str:
8888
result.append(char.lower())
8989
return "".join(result)
9090

91+
def env_variable_name(self) -> str:
92+
"""Convert property name to environment variable name format.
93+
Example: sonar.scanner.proxyPort -> SONAR_SCANNER_PROXY_PORT"""
94+
# Replace dots with underscores
95+
env_name = self.name.replace(".", "_")
96+
97+
# Insert underscores before uppercase letters (camelCase to snake_case)
98+
result = []
99+
for i, char in enumerate(env_name):
100+
if char.isupper() and i > 0 and env_name[i - 1] != "_":
101+
result.append("_")
102+
result.append(char)
103+
# Convert to uppercase
104+
return "".join(result).upper()
105+
91106

92107
# fmt: off
93108
PROPERTIES: list[Property] = [

tests/test_configuration_loader.py

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
TOML_PATH,
4545
SONAR_PROJECT_DESCRIPTION,
4646
SONAR_PYTHON_VERSION,
47+
SONAR_HOST_URL,
48+
SONAR_SCANNER_JAVA_OPTS,
4749
)
4850
from pysonar_scanner.configuration.configuration_loader import ConfigurationLoader, SONAR_PROJECT_BASE_DIR
4951
from pysonar_scanner.exceptions import MissingKeyException
@@ -53,6 +55,8 @@ class TestConfigurationLoader(pyfakefs.TestCase):
5355
def setUp(self):
5456
self.maxDiff = None
5557
self.setUpPyfakefs()
58+
self.env_patcher = patch.dict("os.environ", {}, clear=True)
59+
self.env_patcher.start()
5660

5761
@patch("sys.argv", ["myscript.py", "--token", "myToken", "--sonar-project-key", "myProjectKey"])
5862
def test_defaults(self):
@@ -265,6 +269,19 @@ def test_load_pyproject_toml_from_toml_path(self):
265269
}
266270
self.assertDictEqual(configuration, expected_configuration)
267271

272+
@patch("sys.argv", ["myscript.py"])
273+
@patch.dict("os.environ", {"SONAR_TOKEN": "TokenFromEnv", "SONAR_PROJECT_KEY": "KeyFromEnv"}, clear=True)
274+
def test_load_from_env_variables_only(self):
275+
"""Test that configuration can be loaded exclusively from environment variables"""
276+
configuration = ConfigurationLoader.load()
277+
278+
# Check that environment variables are loaded correctly
279+
self.assertEqual(configuration[SONAR_TOKEN], "TokenFromEnv")
280+
self.assertEqual(configuration[SONAR_PROJECT_KEY], "KeyFromEnv")
281+
282+
# Default values should still be populated
283+
self.assertEqual(configuration[SONAR_SCANNER_APP], "python")
284+
268285
@patch(
269286
"sys.argv",
270287
[
@@ -275,18 +292,38 @@ def test_load_pyproject_toml_from_toml_path(self):
275292
"ProjectKeyFromCLI",
276293
],
277294
)
278-
def test_properties_and_toml_priority(self):
279-
"""Test that sonar-project.properties has priority over pyproject.toml when both exist"""
295+
@patch.dict(
296+
"os.environ",
297+
{
298+
"SONAR_TOKEN": "TokenFromEnv", # Should be overridden by CLI
299+
"SONAR_HOST_URL": "https://sonar.env.example.com", # Not set elsewhere, should be used
300+
"SONAR_USER_HOME": "/env/sonar/home", # Should be used (overriding sonar-project.properties)
301+
"SONAR_SCANNER_JAVA_OPTS": "-Xmx2048m", # Unique to env vars
302+
},
303+
clear=True,
304+
)
305+
def test_properties_priority(self):
306+
"""Test the priority order of different configuration sources:
307+
1. CLI args (highest)
308+
2. Environment variables
309+
3. Generic environment variable
310+
4. pyproject.toml [tool.sonar] section
311+
5. sonar-project.properties
312+
6. Generic properties from pyproject.toml [project] section
313+
7. Default values (lowest)
314+
"""
280315
# Create both configuration files
281316
self.fs.create_file(
282317
"sonar-project.properties",
283318
contents=(
284319
"""
285320
sonar.projectKey=ProjectKeyFromProperties
286321
sonar.projectName=Properties Project
322+
sonar.projectDescription=Properties Project Description
287323
sonar.sources=src/properties
288324
sonar.tests=test/properties
289325
sonar.exclusions=properties-exclusions/**/*
326+
sonar.userHome=/properties/sonar/home
290327
"""
291328
),
292329
)
@@ -303,23 +340,35 @@ def test_properties_and_toml_priority(self):
303340
project-name = "TOML Project"
304341
sources = "src/toml"
305342
exclusions = "toml-exclusions/**/*"
343+
userHome = "/toml/sonar/home"
306344
"""
307345
),
308346
)
309347

310348
configuration = ConfigurationLoader.load()
311349

312-
# Generic pyproject.toml properties are retrieved when no other source is available
313-
self.assertEqual(configuration[SONAR_PROJECT_DESCRIPTION], "My Project Description")
350+
# Test Default values (lowest priority)
351+
self.assertNotEqual(configuration[SONAR_SCANNER_CONNECT_TIMEOUT], "5") # Default value would be 5
352+
353+
# Generic pyproject.toml properties from [project] section
314354
self.assertEqual(configuration[SONAR_PYTHON_VERSION], "3.6,3.7,3.8")
315355

316-
# sonar-project.properties values have priority over generic properties from pyproject.toml
317-
self.assertEqual(configuration[SONAR_PROJECT_NAME], "TOML Project")
356+
# sonar-project.properties values
357+
self.assertEqual(configuration[SONAR_TESTS], "test/properties")
358+
self.assertEqual(
359+
configuration[SONAR_PROJECT_DESCRIPTION], "Properties Project Description"
360+
) # Overrides [project] toml
318361

319-
# Sonar pyproject.toml values have priority over sonar-project.properties
362+
# pyproject.toml [tool.sonar] section overrides sonar-project.properties
320363
self.assertEqual(configuration[SONAR_SOURCES], "src/toml")
321364
self.assertEqual(configuration[SONAR_EXCLUSIONS], "toml-exclusions/**/*")
365+
self.assertEqual(configuration[SONAR_PROJECT_NAME], "TOML Project") # Overrides sonar-project.properties
322366

323-
# CLI args still have highest priority
367+
# Environment variables override pyproject.toml [tool.sonar]
368+
self.assertEqual(configuration[SONAR_HOST_URL], "https://sonar.env.example.com")
369+
self.assertEqual(configuration[SONAR_SCANNER_JAVA_OPTS], "-Xmx2048m")
370+
self.assertEqual(configuration[SONAR_USER_HOME], "/env/sonar/home") # Env var overrides [sonar] toml
371+
372+
# CLI args have highest priority
324373
self.assertEqual(configuration[SONAR_PROJECT_KEY], "ProjectKeyFromCLI")
325-
self.assertEqual(configuration[SONAR_TESTS], "test/properties")
374+
self.assertEqual(configuration[SONAR_TOKEN], "myToken") # CLI overrides env var

tests/test_environment_variables.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
#
2+
# Sonar Scanner Python
3+
# Copyright (C) 2011-2024 SonarSource SA.
4+
# mailto:info AT sonarsource DOT com
5+
#
6+
# This program is free software; you can redistribute it and/or
7+
# modify it under the terms of the GNU Lesser General Public
8+
# License as published by the Free Software Foundation; either
9+
# version 3 of the License, or (at your option) any later version.
10+
# This program is distributed in the hope that it will be useful,
11+
#
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
# Lesser General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU Lesser General Public License
17+
# along with this program; if not, write to the Free Software Foundation,
18+
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
#
20+
import unittest
21+
from unittest.mock import patch
22+
23+
from pysonar_scanner.configuration import environment_variables
24+
from pysonar_scanner.configuration.properties import (
25+
SONAR_HOST_URL,
26+
SONAR_REGION,
27+
SONAR_SCANNER_ARCH,
28+
SONAR_SCANNER_JAVA_OPTS,
29+
SONAR_SCANNER_OS,
30+
SONAR_TOKEN,
31+
SONAR_USER_HOME,
32+
SONAR_PROJECT_KEY,
33+
)
34+
35+
36+
class TestEnvironmentVariables(unittest.TestCase):
37+
def setUp(self):
38+
self.maxDiff = None
39+
40+
def test_empty_environment(self):
41+
with patch.dict("os.environ", {}, clear=True):
42+
properties = environment_variables.load()
43+
self.assertEqual(len(properties), 0)
44+
self.assertDictEqual(properties, {})
45+
46+
def test__environment_variables(self):
47+
env = {
48+
"SONAR_TOKEN": "my-token",
49+
"SONAR_HOST_URL": "https://sonarqube.example.com",
50+
"SONAR_USER_HOME": "/custom/sonar/home",
51+
"SONAR_SCANNER_JAVA_OPTS": "-Xmx1024m -XX:MaxPermSize=256m",
52+
"SONAR_REGION": "us",
53+
}
54+
with patch.dict("os.environ", env, clear=True):
55+
properties = environment_variables.load()
56+
expected_properties = {
57+
SONAR_TOKEN: "my-token",
58+
SONAR_HOST_URL: "https://sonarqube.example.com",
59+
SONAR_USER_HOME: "/custom/sonar/home",
60+
SONAR_SCANNER_JAVA_OPTS: "-Xmx1024m -XX:MaxPermSize=256m",
61+
SONAR_REGION: "us",
62+
}
63+
self.assertEqual(len(properties), 5)
64+
self.assertDictEqual(properties, expected_properties)
65+
66+
def test_irrelevant_environment_variables(self):
67+
env = {"UNRELATED_VAR": "some-value", "PATH": "/usr/bin:/bin", "HOME": "/home/user"}
68+
with patch.dict("os.environ", env, clear=True):
69+
properties = environment_variables.load()
70+
self.assertEqual(len(properties), 0)
71+
self.assertDictEqual(properties, {})
72+
73+
def test_mixed_environment_variables(self):
74+
env = {
75+
"SONAR_TOKEN": "my-token",
76+
"SONAR_HOST_URL": "https://sonarqube.example.com",
77+
"SONAR_PROJECT_KEY": "MyProjectKey",
78+
"UNRELATED_VAR": "some-value",
79+
"SONAR_SCANNER_OS": "linux",
80+
"SONAR_SCANNER_ARCH": "x64",
81+
"PATH": "/usr/bin:/bin",
82+
}
83+
with patch.dict("os.environ", env, clear=True):
84+
properties = environment_variables.load()
85+
expected_properties = {
86+
SONAR_TOKEN: "my-token",
87+
SONAR_HOST_URL: "https://sonarqube.example.com",
88+
SONAR_PROJECT_KEY: "MyProjectKey",
89+
SONAR_SCANNER_OS: "linux",
90+
SONAR_SCANNER_ARCH: "x64",
91+
}
92+
self.assertEqual(len(properties), 5)
93+
self.assertDictEqual(properties, expected_properties)

0 commit comments

Comments
 (0)