Skip to content

Commit 12843ff

Browse files
committed
feat: add developer runtime config endpoint
1 parent 69d079b commit 12843ff

2 files changed

Lines changed: 155 additions & 9 deletions

File tree

server/endpoints.py

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,67 @@
1-
"""
2-
This is the file containing all of the endpoints for our flask app.
3-
The endpoint called `endpoints` will return all available endpoints.
4-
"""
1+
"""General-purpose API endpoints."""
52

3+
import os
64
from http import HTTPStatus
75

86
from flask_restx import Resource, Namespace
97

108
import data.countries as countries_db
119
import data.states as states_db
10+
from data.db_connect import CLOUD, LOCAL, SE_DB
11+
from server.app import (
12+
APP_NAME,
13+
get_cache_enabled,
14+
get_runtime_environment,
15+
get_runtime_log_level,
16+
get_runtime_port,
17+
get_runtime_version,
18+
)
1219

1320
# Create namespace for each resource
1421
general_ns = Namespace("general", description="General API operations")
15-
countries_ns = Namespace(
16-
"countries", description="Operations related to countries"
17-
)
22+
countries_ns = Namespace("countries", description="Operations related to countries")
1823
states_ns = Namespace("states", description="Operations related to states")
1924

2025
# Constants for endpoints and responses
2126
HELLO_EP = "/hello"
2227
HELLO_RESP = "hello"
2328

2429

30+
def _parse_feature_flag(raw_value: str) -> bool | int | str:
31+
lowered = raw_value.lower()
32+
if lowered in {"true", "false"}:
33+
return lowered == "true"
34+
if raw_value.isdigit():
35+
return int(raw_value)
36+
return raw_value
37+
38+
39+
def _get_feature_flags() -> dict[str, bool | int | str]:
40+
feature_flags = {}
41+
for key, value in os.environ.items():
42+
if key.startswith("FEATURE_"):
43+
feature_name = key.removeprefix("FEATURE_").lower()
44+
feature_flags[feature_name] = _parse_feature_flag(value)
45+
return dict(sorted(feature_flags.items()))
46+
47+
48+
def _get_build_metadata() -> dict[str, str]:
49+
metadata_fields = {
50+
"commit_sha": os.getenv("GIT_SHA"),
51+
"build_id": os.getenv("BUILD_ID"),
52+
"release_id": os.getenv("RELEASE_ID"),
53+
"deploy_id": os.getenv("DEPLOY_ID"),
54+
}
55+
return {key: value for key, value in metadata_fields.items() if value}
56+
57+
58+
def _get_safe_database_config() -> dict[str, str | bool]:
59+
return {
60+
"name": os.getenv("DB_NAME", SE_DB),
61+
"mode": "cloud" if os.getenv("CLOUD_MONGO", LOCAL) == CLOUD else "local",
62+
}
63+
64+
2565
@general_ns.route("/hello")
2666
class HelloWorld(Resource):
2767
"""
@@ -76,6 +116,27 @@ def get(self):
76116
The `get()` method will return a sorted list of available endpoints.
77117
"""
78118
from flask import current_app
79-
endpoints = sorted(rule.rule for rule in
80-
current_app.url_map.iter_rules())
119+
120+
endpoints = sorted(rule.rule for rule in current_app.url_map.iter_rules())
81121
return {"Available endpoints": endpoints}
122+
123+
124+
@general_ns.route("/dev/config")
125+
class DevConfig(Resource):
126+
"""Return curated runtime configuration that is safe to expose."""
127+
128+
def get(self):
129+
payload = {
130+
"app_name": APP_NAME,
131+
"environment": get_runtime_environment(),
132+
"version": get_runtime_version(),
133+
"port": get_runtime_port(),
134+
"log_level": get_runtime_log_level(),
135+
"feature_flags": _get_feature_flags(),
136+
"database": _get_safe_database_config(),
137+
"cache_enabled": get_cache_enabled(),
138+
}
139+
build_metadata = _get_build_metadata()
140+
if build_metadata:
141+
payload["build"] = build_metadata
142+
return payload, HTTPStatus.OK

server/tests/test_dev_config.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import os
2+
3+
from server.app import create_app
4+
5+
6+
def test_dev_config_returns_safe_runtime_configuration(monkeypatch):
7+
monkeypatch.setenv("APP_ENV", "production")
8+
monkeypatch.setenv("PORT", "9090")
9+
monkeypatch.setenv("LOG_LEVEL", "debug")
10+
monkeypatch.setenv("FEATURE_BETA_DASHBOARD", "true")
11+
monkeypatch.setenv("FEATURE_METRICS_ENABLED", "false")
12+
monkeypatch.setenv("DB_NAME", "geo")
13+
monkeypatch.setenv("CLOUD_MONGO", "1")
14+
monkeypatch.setenv("GIT_SHA", "abc123")
15+
monkeypatch.setenv("JWT_SECRET", "super-secret")
16+
monkeypatch.setenv("LOCAL_MONGO_DB_URI", "mongodb://user:pass@localhost:27017/")
17+
monkeypatch.setenv(
18+
"ATLAS_MONGO_DB_URI", "mongodb+srv://user:pass@example.mongodb.net/"
19+
)
20+
21+
app = create_app()
22+
with app.test_client() as client:
23+
response = client.get("/dev/config")
24+
25+
payload = response.get_json()
26+
response_text = response.get_data(as_text=True)
27+
28+
assert response.status_code == 200
29+
assert payload == {
30+
"app_name": "Geographic Database API",
31+
"environment": "production",
32+
"version": "v1",
33+
"port": 9090,
34+
"log_level": "DEBUG",
35+
"feature_flags": {
36+
"beta_dashboard": True,
37+
"metrics_enabled": False,
38+
},
39+
"database": {
40+
"name": "geo",
41+
"mode": "cloud",
42+
},
43+
"cache_enabled": True,
44+
"build": {
45+
"commit_sha": "abc123",
46+
},
47+
}
48+
assert "super-secret" not in response_text
49+
assert "mongodb://user:pass@localhost:27017/" not in response_text
50+
assert "mongodb+srv://user:pass@example.mongodb.net/" not in response_text
51+
assert "JWT_SECRET" not in response_text
52+
53+
54+
def test_dev_config_omits_optional_build_metadata_when_absent(monkeypatch):
55+
for key in [
56+
"APP_ENV",
57+
"PORT",
58+
"LOG_LEVEL",
59+
"DB_NAME",
60+
"CLOUD_MONGO",
61+
"FEATURE_BETA_DASHBOARD",
62+
"FEATURE_METRICS_ENABLED",
63+
"GIT_SHA",
64+
"BUILD_ID",
65+
"RELEASE_ID",
66+
"DEPLOY_ID",
67+
]:
68+
monkeypatch.delenv(key, raising=False)
69+
70+
app = create_app()
71+
with app.test_client() as client:
72+
response = client.get("/dev/config")
73+
74+
payload = response.get_json()
75+
76+
assert response.status_code == 200
77+
assert payload["version"] == "v1"
78+
assert payload["environment"] == "development"
79+
assert payload["port"] == 8000
80+
assert payload["feature_flags"] == {}
81+
assert payload["database"] == {
82+
"name": "seDB",
83+
"mode": "local",
84+
}
85+
assert "build" not in payload

0 commit comments

Comments
 (0)