Skip to content
This repository was archived by the owner on Dec 16, 2025. It is now read-only.

Commit f2c9ffa

Browse files
committed
Added secrets loading
1 parent a298be3 commit f2c9ffa

File tree

2 files changed

+92
-3
lines changed

2 files changed

+92
-3
lines changed

make87/config.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,35 @@
11
import os
2+
import re
23
from typing import Union, Dict, TypeVar, Callable, Any
34

45
from make87.models import ApplicationConfig
56

67
CONFIG_ENV_VAR = "MAKE87_CONFIG"
78

9+
# Match pattern: ${secret.XYZ} with optional whitespace
10+
SECRET_PATTERN = re.compile(r"^\s*\$\{secret\.([A-Za-z0-9_]+)\}\s*$")
11+
12+
13+
def _resolve_secrets(obj):
14+
# Recursively resolve secrets in dicts/lists
15+
if isinstance(obj, dict):
16+
return {k: _resolve_secrets(v) for k, v in obj.items()}
17+
elif isinstance(obj, list):
18+
return [_resolve_secrets(item) for item in obj]
19+
elif isinstance(obj, str):
20+
match = SECRET_PATTERN.match(obj)
21+
if match:
22+
secret_name = match.group(1)
23+
secret_path = f"/run/secrets/{secret_name}.secret"
24+
try:
25+
with open(secret_path, "r") as f:
26+
return f.read().strip()
27+
except Exception as e:
28+
raise RuntimeError(f"Failed to load secret '{secret_name}' from {secret_path}: {e}")
29+
return obj
30+
else:
31+
return obj
32+
833

934
def load_config_from_env(var: str = CONFIG_ENV_VAR) -> ApplicationConfig:
1035
"""
@@ -14,19 +39,23 @@ def load_config_from_env(var: str = CONFIG_ENV_VAR) -> ApplicationConfig:
1439
raw = os.environ.get(var)
1540
if not raw:
1641
raise RuntimeError(f"Required env var {var} missing!")
17-
return ApplicationConfig.model_validate_json(raw)
42+
config = ApplicationConfig.model_validate_json(raw)
43+
config.config = _resolve_secrets(config.config)
44+
return config
1845

1946

2047
def load_config_from_json(json_data: Union[str, Dict]) -> ApplicationConfig:
2148
"""
2249
Load and validate ApplicationConfig from a JSON string or dict.
2350
"""
2451
if isinstance(json_data, str):
25-
return ApplicationConfig.model_validate_json(json_data)
52+
config = ApplicationConfig.model_validate_json(json_data)
2653
elif isinstance(json_data, dict):
27-
return ApplicationConfig.model_validate(json_data)
54+
config = ApplicationConfig.model_validate(json_data)
2855
else:
2956
raise TypeError("json_data must be a JSON string or dict.")
57+
config.config = _resolve_secrets(config.config)
58+
return config
3059

3160

3261
T = TypeVar("T")

tests/unit/config/test_config.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import os
2+
import tempfile
3+
import pytest
4+
5+
from make87.config import load_config_from_json, get_config_value
6+
7+
8+
class DummyAppConfig:
9+
def __init__(self, config):
10+
self.config = config
11+
12+
13+
def test_secret_resolution(monkeypatch):
14+
# Create a temporary secret file
15+
with tempfile.TemporaryDirectory() as tmpdir:
16+
secret_name = "MYSECRET"
17+
secret_value = "supersecret"
18+
secret_file = os.path.join(tmpdir, f"{secret_name}.secret")
19+
with open(secret_file, "w") as f:
20+
f.write(secret_value)
21+
22+
# Patch open to redirect /run/secrets/MYSECRET.secret to our temp file
23+
import builtins
24+
25+
real_open = builtins.open
26+
27+
def fake_open(path, *args, **kwargs):
28+
if path == f"/run/secrets/{secret_name}.secret":
29+
return real_open(secret_file, *args, **kwargs)
30+
return real_open(path, *args, **kwargs)
31+
32+
monkeypatch.setattr("builtins.open", fake_open)
33+
34+
# Provide all required fields for ApplicationConfig
35+
config_dict = {
36+
"application_info": {
37+
"application_id": "app-id",
38+
"application_name": "dummy",
39+
"deployed_application_id": "deploy-id",
40+
"deployed_application_name": "dummy-deploy",
41+
"is_release_version": False,
42+
"name": "dummy", # legacy/extra, ignored if not in model
43+
"system_id": "sys-id",
44+
"version": "1.0",
45+
},
46+
"interfaces": {},
47+
"peripherals": {"peripherals": []},
48+
"config": {"password": "${secret.MYSECRET}"},
49+
}
50+
config = load_config_from_json(config_dict)
51+
assert config.config["password"] == secret_value
52+
53+
54+
def test_get_config_value():
55+
config = DummyAppConfig({"foo": 123, "bar": "baz"})
56+
assert get_config_value(config, "foo") == 123
57+
assert get_config_value(config, "bar") == "baz"
58+
assert get_config_value(config, "missing", default="x") == "x"
59+
with pytest.raises(KeyError):
60+
get_config_value(config, "missing2")

0 commit comments

Comments
 (0)