Skip to content

Commit e9e1f20

Browse files
refactor: use regex pattern for SUPERVISOR_ env var validation
- Replace manual string parsing with compiled regex pattern - Explicitly validate section/key format (no leading/trailing underscores) - Add comprehensive test coverage for edge cases - Improve code maintainability and clarity
1 parent 33dcba5 commit e9e1f20

File tree

2 files changed

+105
-38
lines changed

2 files changed

+105
-38
lines changed

python/model_hosting_container_standards/supervisor/models.py

Lines changed: 25 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -118,55 +118,42 @@ def _parse_supervisor_custom_sections() -> Dict[str, Dict[str, str]]:
118118
Returns:
119119
Dictionary mapping section names to their key-value configurations
120120
"""
121+
import re
122+
123+
# Pattern matches SUPERVISOR_SECTION_KEY where:
124+
# - SECTION: alphanumeric, may contain __ (for colons) or _ (internal), no leading/trailing _
125+
# - KEY: alphanumeric, may contain _ (internal), no leading/trailing _
126+
pattern = re.compile(
127+
r"^SUPERVISOR_"
128+
r"(?P<section>[A-Z0-9]+(?:__[A-Z0-9]+|_[A-Z0-9]+)*)" # SECTION (__ for colons)
129+
r"_(?P<key>[A-Z0-9]+(?:_[A-Z0-9]+)*)$" # KEY (no leading/trailing _)
130+
)
131+
121132
custom_sections: Dict[str, Dict[str, str]] = {}
122133

123134
for env_var, value in os.environ.items():
124-
if not env_var.startswith("SUPERVISOR_"):
125-
continue
126-
127-
# Skip the config path variable
135+
# Skip non-SUPERVISOR_ variables and the config path variable
128136
if env_var == "SUPERVISOR_CONFIG_PATH":
129137
continue
130138

131-
# Remove SUPERVISOR_ prefix
132-
remaining = env_var[11:] # len("SUPERVISOR_") = 11
133-
134-
# Find the last underscore to separate key from section
135-
last_underscore = remaining.rfind("_")
136-
if last_underscore == -1:
137-
logger.warning(
138-
f"Invalid SUPERVISOR_ environment variable format: '{env_var}'. "
139-
f"Expected format: SUPERVISOR_SECTION_KEY=value"
140-
)
139+
match = pattern.match(env_var)
140+
if not match:
141+
# Only warn if it starts with SUPERVISOR_ but doesn't match pattern
142+
if env_var.startswith("SUPERVISOR_"):
143+
logger.warning(
144+
f"Invalid SUPERVISOR_ environment variable format: '{env_var}'. "
145+
f"Expected format: SUPERVISOR_SECTION_KEY=value (alphanumeric with underscores, "
146+
f"no leading/trailing underscores, use __ for section colons)"
147+
)
141148
continue
142149

143-
section_part = remaining[:last_underscore]
144-
key_name = remaining[last_underscore + 1 :].lower()
150+
# Extract section and key from regex groups
151+
section_part = match.group("section")
152+
key_name = match.group("key").lower()
145153

146-
# Convert double underscores to colons in section name first
154+
# Convert double underscores to colons in section name
147155
section_name = section_part.replace("__", ":").lower()
148156

149-
# Validate section and key are not empty after processing
150-
# Also check for invalid section names (starting with underscore indicates empty section before __)
151-
if (
152-
not section_name
153-
or section_name.startswith(":")
154-
or section_name.endswith(":")
155-
or section_name.startswith("_")
156-
):
157-
logger.warning(
158-
f"Invalid SUPERVISOR_ environment variable: '{env_var}' has invalid section name. "
159-
f"Expected format: SUPERVISOR_SECTION_KEY=value"
160-
)
161-
continue
162-
163-
if not key_name:
164-
logger.warning(
165-
f"Invalid SUPERVISOR_ environment variable: '{env_var}' has empty key name. "
166-
f"Expected format: SUPERVISOR_SECTION_KEY=value"
167-
)
168-
continue
169-
170157
# Initialize section if it doesn't exist
171158
if section_name not in custom_sections:
172159
custom_sections[section_name] = {}

python/tests/supervisor/test_models.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,86 @@ def test_invalid_format_ignored(self):
294294
# All invalid formats should be ignored, result should be empty
295295
assert result == {}
296296

297+
def test_leading_underscore_in_section_rejected(self):
298+
"""Test that section names with leading underscores are rejected."""
299+
test_env = {
300+
"SUPERVISOR__PROGRAM_COMMAND": "python app.py", # Leading underscore in section
301+
}
302+
303+
with patch.dict(os.environ, test_env, clear=True):
304+
result = _parse_supervisor_custom_sections()
305+
assert result == {}
306+
307+
def test_trailing_underscore_in_section_rejected(self):
308+
"""Test that section names with trailing underscores are rejected."""
309+
test_env = {
310+
"SUPERVISOR_PROGRAM__COMMAND": "python app.py", # Trailing underscore in section (before key)
311+
}
312+
313+
with patch.dict(os.environ, test_env, clear=True):
314+
result = _parse_supervisor_custom_sections()
315+
assert result == {}
316+
317+
def test_multiple_consecutive_underscores_rejected(self):
318+
"""Test that three or more consecutive underscores are rejected."""
319+
test_env = {
320+
"SUPERVISOR_PROGRAM___WEB_COMMAND": "gunicorn app:app", # Three underscores
321+
}
322+
323+
with patch.dict(os.environ, test_env, clear=True):
324+
result = _parse_supervisor_custom_sections()
325+
assert result == {}
326+
327+
def test_leading_underscore_in_key_rejected(self):
328+
"""Test that key names with leading underscores are rejected."""
329+
test_env = {
330+
"SUPERVISOR_PROGRAM__COMMAND": "python app.py", # Leading underscore in key
331+
}
332+
333+
with patch.dict(os.environ, test_env, clear=True):
334+
result = _parse_supervisor_custom_sections()
335+
assert result == {}
336+
337+
def test_trailing_underscore_in_key_rejected(self):
338+
"""Test that key names with trailing underscores are rejected."""
339+
test_env = {
340+
"SUPERVISOR_PROGRAM_COMMAND_": "python app.py", # Trailing underscore in key
341+
}
342+
343+
with patch.dict(os.environ, test_env, clear=True):
344+
result = _parse_supervisor_custom_sections()
345+
assert result == {}
346+
347+
def test_numeric_only_sections_and_keys_accepted(self):
348+
"""Test that purely numeric section and key names are accepted."""
349+
test_env = {
350+
"SUPERVISOR_123_456": "value", # Numeric section and key
351+
}
352+
353+
with patch.dict(os.environ, test_env, clear=True):
354+
result = _parse_supervisor_custom_sections()
355+
356+
expected = {
357+
"123": {"456": "value"},
358+
}
359+
assert result == expected
360+
361+
def test_mixed_alphanumeric_accepted(self):
362+
"""Test that mixed alphanumeric section and key names are accepted."""
363+
test_env = {
364+
"SUPERVISOR_PROGRAM2_COMMAND3": "python app.py",
365+
"SUPERVISOR_WEB1__API2_PORT8080": "8080",
366+
}
367+
368+
with patch.dict(os.environ, test_env, clear=True):
369+
result = _parse_supervisor_custom_sections()
370+
371+
expected = {
372+
"program2": {"command3": "python app.py"},
373+
"web1:api2": {"port8080": "8080"},
374+
}
375+
assert result == expected
376+
297377

298378
class TestParseEnvironmentVariables:
299379
"""Test the main parse_environment_variables function."""

0 commit comments

Comments
 (0)