Skip to content

Commit 956923d

Browse files
authored
[AAP-69236] Enable startup validation for production environment variables (#193)
* Enable startup validation for production environment variables * Formatting fix * Updated tests * Uncomment SEGMENT_WRITE_KEY fix: SEGMENT_WRITE_KEY is baked into container image at build time, so it should always be present even in air-gapped environments. Validator ensures proper configuration at startup * Add comprehensive test coverage for production validator enforcement * Refactor validator tests * Add module-level import test for production validator coverage * update sonar properties * Removed brittle tests after updating sonarcloud configuration
1 parent f10fb0a commit 956923d

4 files changed

Lines changed: 76 additions & 50 deletions

File tree

apps/settings/production.py

Lines changed: 58 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -75,33 +75,38 @@
7575
),
7676
)
7777

78-
# Optional until gateway APIs are implemented; uncomment validator when re-adding.
7978
RESOURCE_SERVER__SECRET_KEY = ""
80-
# validators.append(
81-
# Validator(
82-
# "RESOURCE_SERVER__SECRET_KEY",
83-
# must_exist=True,
84-
# ne="",
85-
# messages={"operations": "RESOURCE_SERVER__SECRET_KEY must be set."},
86-
# ),
87-
# )
79+
validators.append(
80+
Validator(
81+
"RESOURCE_SERVER__SECRET_KEY",
82+
must_exist=True,
83+
ne="",
84+
messages={
85+
"operations": (
86+
"RESOURCE_SERVER__SECRET_KEY must be set. "
87+
"Set METRICS_SERVICE_RESOURCE_SERVER__SECRET_KEY environment variable."
88+
)
89+
},
90+
),
91+
)
8892

8993
# =============================================================================
9094
# Authentication
9195
# =============================================================================
9296

93-
# Optional until gateway APIs are implemented; uncomment validator when re-adding.
9497
ANSIBLE_BASE_JWT_KEY = ""
95-
# validators.append(
96-
# Validator(
97-
# "ANSIBLE_BASE_JWT_KEY",
98-
# must_exist=True,
99-
# ne="",
100-
# messages={
101-
# "operations": ("ANSIBLE_BASE_JWT_KEY must be set. "),
102-
# },
103-
# ),
104-
# )
98+
validators.append(
99+
Validator(
100+
"ANSIBLE_BASE_JWT_KEY",
101+
must_exist=True,
102+
ne="",
103+
messages={
104+
"operations": (
105+
"ANSIBLE_BASE_JWT_KEY must be set. Set METRICS_SERVICE_ANSIBLE_BASE_JWT_KEY environment variable."
106+
)
107+
},
108+
),
109+
)
105110

106111
REST_FRAMEWORK__DEFAULT_AUTHENTICATION_CLASSES = [
107112
"apps.core.authentication.ServiceJWTAuthentication",
@@ -202,16 +207,26 @@
202207
# External Services
203208
# =============================================================================
204209

205-
# Optional until gateway APIs are implemented; uncomment validator when re-adding.
210+
# Loaded from file at startup via _load_segment_write_key_from_file().
211+
# Default path: /etc/ansible-automation-platform/metrics/segment-write-key
212+
# Override path via METRICS_SERVICE_SEGMENT_WRITE_KEY_FILE
213+
# Key is baked into container image at build time - always present even in air-gapped environments.
206214
SEGMENT_WRITE_KEY = ""
207-
# validators.append(
208-
# Validator(
209-
# "SEGMENT_WRITE_KEY",
210-
# must_exist=True,
211-
# ne="",
212-
# messages={"operations": "SEGMENT_WRITE_KEY must be set."},
213-
# ),
214-
# )
215+
validators.append(
216+
Validator(
217+
"SEGMENT_WRITE_KEY",
218+
must_exist=True,
219+
ne="",
220+
messages={
221+
"operations": (
222+
"SEGMENT_WRITE_KEY must be set. "
223+
"Either set METRICS_SERVICE_SEGMENT_WRITE_KEY environment variable "
224+
"or provide the key file at /etc/ansible-automation-platform/metrics/segment-write-key "
225+
"(or path specified in METRICS_SERVICE_SEGMENT_WRITE_KEY_FILE)."
226+
)
227+
},
228+
),
229+
)
215230

216231
# =============================================================================
217232
# Static Files
@@ -232,16 +247,21 @@
232247
# =============================================================================
233248
# Allowed Hosts
234249
# =============================================================================
235-
# Optional until gateway APIs are implemented; uncomment validator when re-adding.
236250
# Set via METRICS_SERVICE_ALLOWED_HOSTS (comma-separated or JSON array).
237251
# Example: METRICS_SERVICE_ALLOWED_HOSTS=metrics.example.com,api.example.com
238252
# Or: METRICS_SERVICE_ALLOWED_HOSTS='["metrics.example.com","api.example.com"]'
239253
ALLOWED_HOSTS = []
240-
# validators.append(
241-
# Validator(
242-
# "ALLOWED_HOSTS",
243-
# must_exist=True,
244-
# condition=lambda v: isinstance(v, list) and len(v) > 0,
245-
# messages={"condition": "ALLOWED_HOSTS must be set via METRICS_SERVICE_ALLOWED_HOSTS in production."},
246-
# ),
247-
# )
254+
validators.append(
255+
Validator(
256+
"ALLOWED_HOSTS",
257+
must_exist=True,
258+
condition=lambda v: isinstance(v, list) and len(v) > 0,
259+
messages={
260+
"condition": (
261+
"ALLOWED_HOSTS must be set via METRICS_SERVICE_ALLOWED_HOSTS in production. "
262+
"Django will reject all requests with an empty ALLOWED_HOSTS list. "
263+
"Set METRICS_SERVICE_ALLOWED_HOSTS to a comma-separated list or JSON array of hostnames."
264+
)
265+
},
266+
),
267+
)

metrics_service/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,6 @@ def _load_segment_write_key_from_file(
469469
DYNACONF.set("DATABASES", _databases)
470470

471471
# Update django.conf.settings with DYNACONF keys.
472-
export(__name__, DYNACONF, validation=False)
472+
export(__name__, DYNACONF, validation=True)
473473

474474
## --- End Settings | After this line only post validation can happen --- #

sonar-project.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ sonar.organization=ansible
1212

1313
# Customize what paths to scan. Default is .
1414
sonar.sources=.
15-
sonar.exclusions=**/tests/**,**/migrations/**,**/tools/**,.venv/**,scripts/**,htmlcov/**
15+
sonar.exclusions=**/tests/**,**/migrations/**,**/tools/**,.venv/**,scripts/**,htmlcov/**,**/settings/**
1616

1717
# Verbose name of project displayed in WUI. Default is set to the projectKey. This field is optional.
1818
sonar.projectName=metrics-service
@@ -23,7 +23,7 @@ sonar.projectName=metrics-service
2323
# Tell sonar scanner where coverage files exist
2424
sonar.python.coverage.reportPaths=coverage.xml
2525
# Exclude certain directories from code coverage reporting
26-
sonar.coverage.exclusions=**/tests/**,**/tools**,**/migrations/**
26+
sonar.coverage.exclusions=**/tests/**,**/tools**,**/migrations/**,**/settings/**
2727
# Manages code duplication sensitivity. Default is 100. Higher number means less sensitive.
2828
sonar.cpd.python.minimumTokens=150
2929
sonar.cpd.python.minimumLines=300

tests/unit/test_dynaconf_settings.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -87,27 +87,33 @@ def test_database_validators_configured(self):
8787
db_validators = [v for v in validators if any("DATABASES" in str(name) for name in v.names)]
8888
assert len(db_validators) > 0
8989

90-
def test_optional_production_settings_have_no_validators(self):
91-
"""Test that optional production settings are not required by validators.
90+
def test_required_production_settings_have_validators(self):
91+
"""Test that required production settings have validators.
9292
9393
RESOURCE_SERVER__SECRET_KEY, ANSIBLE_BASE_JWT_KEY, SEGMENT_WRITE_KEY, and
94-
ALLOWED_HOSTS are optional in production so deployments can start without
95-
gateway auth, Segment, or explicit allowed hosts when not needed.
94+
ALLOWED_HOSTS are now required in production to ensure proper AAP gateway
95+
integration and prevent misconfigured deployments.
96+
97+
SEGMENT_WRITE_KEY is baked into the container image at build time, so it
98+
should always be present even in air-gapped environments.
9699
"""
97100
from apps.settings.production import validators
98101

99-
optional_settings = {
102+
required_settings = {
100103
"RESOURCE_SERVER__SECRET_KEY",
101104
"ANSIBLE_BASE_JWT_KEY",
102105
"SEGMENT_WRITE_KEY",
103106
"ALLOWED_HOSTS",
104107
}
108+
109+
found_validators = set()
105110
for v in validators:
106111
for name in v.names:
107-
assert name not in optional_settings, (
108-
f"Setting {name!r} should be optional (no validator); "
109-
"remove its validator from apps/settings/production.py"
110-
)
112+
if name in required_settings:
113+
found_validators.add(name)
114+
115+
# All required settings should have validators
116+
assert found_validators == required_settings, f"Missing validators for: {required_settings - found_validators}"
111117

112118
def test_settings_pass_validation(self):
113119
"""Test that current settings pass validation (since we're running)."""

0 commit comments

Comments
 (0)