dbos 0.25.0a16__tar.gz → 0.26.0a1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dbos-0.25.0a16 → dbos-0.26.0a1}/PKG-INFO +1 -1
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_dbos_config.py +30 -3
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/dbos-config.schema.json +1 -1
- {dbos-0.25.0a16 → dbos-0.26.0a1}/pyproject.toml +1 -1
- dbos-0.26.0a1/tests/test_docker_secrets.py +521 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/LICENSE +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/README.md +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/__init__.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/__main__.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_admin_server.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_app_db.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_classproperty.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_client.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_cloudutils/authentication.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_cloudutils/cloudutils.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_cloudutils/databases.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_conductor/conductor.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_conductor/protocol.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_context.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_core.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_croniter.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_db_wizard.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_dbos.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_debug.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_error.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_fastapi.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_flask.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_kafka.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_kafka_message.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_logger.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_migrations/env.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_migrations/script.py.mako +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_migrations/versions/f4b9b32ba814_functionname_childid_op_outputs.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_outcome.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_queue.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_recovery.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_registrations.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_request.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_roles.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_scheduler.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_schemas/__init__.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_schemas/application_database.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_schemas/system_database.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_serialization.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_sys_db.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_templates/dbos-db-starter/README.md +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_templates/dbos-db-starter/__package/main.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_tracer.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_utils.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_workflow_commands.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/cli/_github_init.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/cli/_template_init.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/cli/cli.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/py.typed +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/tests/__init__.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/tests/atexit_no_ctor.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/tests/atexit_no_launch.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/tests/classdefs.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/tests/client_collateral.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/tests/client_worker.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/tests/conftest.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/tests/more_classdefs.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/tests/queuedworkflow.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/tests/test_admin_server.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/tests/test_async.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/tests/test_classdecorators.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/tests/test_client.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/tests/test_concurrency.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/tests/test_config.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/tests/test_croniter.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/tests/test_dbos.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/tests/test_dbwizard.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/tests/test_debug.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/tests/test_failures.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/tests/test_fastapi.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/tests/test_fastapi_roles.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/tests/test_flask.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/tests/test_kafka.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/tests/test_outcome.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/tests/test_package.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/tests/test_queue.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/tests/test_scheduler.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/tests/test_schema_migration.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/tests/test_singleton.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/tests/test_spans.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/tests/test_sqlalchemy.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/tests/test_workflow_cancel.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/tests/test_workflow_cmds.py +0 -0
- {dbos-0.25.0a16 → dbos-0.26.0a1}/version/__init__.py +0 -0
@@ -215,9 +215,13 @@ def translate_dbos_config_to_config_file(config: DBOSConfig) -> ConfigFile:
|
|
215
215
|
|
216
216
|
|
217
217
|
def _substitute_env_vars(content: str, silent: bool = False) -> str:
|
218
|
-
regex = r"\$\{([^}]+)\}" # Regex to match ${VAR_NAME} style placeholders
|
219
218
|
|
220
|
-
|
219
|
+
# Regex to match ${DOCKER_SECRET:SECRET_NAME} style placeholders for Docker secrets
|
220
|
+
secret_regex = r"\$\{DOCKER_SECRET:([^}]+)\}"
|
221
|
+
# Regex to match ${VAR_NAME} style placeholders for environment variables
|
222
|
+
env_regex = r"\$\{(?!DOCKER_SECRET:)([^}]+)\}"
|
223
|
+
|
224
|
+
def replace_env_func(match: re.Match[str]) -> str:
|
221
225
|
var_name = match.group(1)
|
222
226
|
value = os.environ.get(
|
223
227
|
var_name, ""
|
@@ -228,7 +232,30 @@ def _substitute_env_vars(content: str, silent: bool = False) -> str:
|
|
228
232
|
)
|
229
233
|
return value
|
230
234
|
|
231
|
-
|
235
|
+
def replace_secret_func(match: re.Match[str]) -> str:
|
236
|
+
secret_name = match.group(1)
|
237
|
+
try:
|
238
|
+
# Docker secrets are stored in /run/secrets/
|
239
|
+
secret_path = f"/run/secrets/{secret_name}"
|
240
|
+
if os.path.exists(secret_path):
|
241
|
+
with open(secret_path, "r") as f:
|
242
|
+
return f.read().strip()
|
243
|
+
elif not silent:
|
244
|
+
dbos_logger.warning(
|
245
|
+
f"Docker secret {secret_name} would be substituted from /run/secrets/{secret_name}, but the file does not exist"
|
246
|
+
)
|
247
|
+
return ""
|
248
|
+
except Exception as e:
|
249
|
+
if not silent:
|
250
|
+
dbos_logger.warning(
|
251
|
+
f"Error reading Docker secret {secret_name}: {str(e)}"
|
252
|
+
)
|
253
|
+
return ""
|
254
|
+
|
255
|
+
# First replace Docker secrets
|
256
|
+
content = re.sub(secret_regex, replace_secret_func, content)
|
257
|
+
# Then replace environment variables
|
258
|
+
return re.sub(env_regex, replace_env_func, content)
|
232
259
|
|
233
260
|
|
234
261
|
def get_dbos_database_url(config_file_path: str = DBOS_CONFIG_PATH) -> str:
|
@@ -40,7 +40,7 @@
|
|
40
40
|
},
|
41
41
|
"password": {
|
42
42
|
"type": ["string", "null"],
|
43
|
-
"description": "The password to use when connecting to the application database. Developers are strongly encouraged to use environment variable substitution to avoid storing secrets in source."
|
43
|
+
"description": "The password to use when connecting to the application database. Developers are strongly encouraged to use environment variable substitution (${VAR_NAME}) or Docker secrets (${DOCKER_SECRET:SECRET_NAME}) to avoid storing secrets in source."
|
44
44
|
},
|
45
45
|
"connectionTimeoutMillis": {
|
46
46
|
"type": "number",
|
@@ -0,0 +1,521 @@
|
|
1
|
+
import os
|
2
|
+
import tempfile
|
3
|
+
import unittest
|
4
|
+
from typing import Any, Dict, List, TypedDict, cast
|
5
|
+
from unittest.mock import mock_open, patch
|
6
|
+
|
7
|
+
from dbos._dbos_config import _substitute_env_vars, load_config
|
8
|
+
|
9
|
+
|
10
|
+
class ConfigFile(TypedDict, total=False):
|
11
|
+
name: str
|
12
|
+
database: Dict[str, Any]
|
13
|
+
database_url: str
|
14
|
+
telemetry: Dict[str, Any]
|
15
|
+
runtimeConfig: Dict[str, List[str]]
|
16
|
+
|
17
|
+
|
18
|
+
class DatabaseConfig(TypedDict, total=False):
|
19
|
+
hostname: str
|
20
|
+
port: int
|
21
|
+
username: str
|
22
|
+
password: str
|
23
|
+
app_db_name: str
|
24
|
+
url: str
|
25
|
+
nested: Dict[str, Any]
|
26
|
+
# Add other fields as needed
|
27
|
+
|
28
|
+
|
29
|
+
class TestDockerSecrets(unittest.TestCase):
|
30
|
+
def setUp(self) -> None:
|
31
|
+
# Create a temporary directory to simulate /run/secrets/
|
32
|
+
self.temp_dir = tempfile.TemporaryDirectory()
|
33
|
+
self.secrets_dir = os.path.join(self.temp_dir.name, "secrets")
|
34
|
+
os.makedirs(self.secrets_dir)
|
35
|
+
|
36
|
+
def tearDown(self) -> None:
|
37
|
+
self.temp_dir.cleanup()
|
38
|
+
|
39
|
+
def test_substitute_env_vars_with_env_vars(self) -> None:
|
40
|
+
# Test that environment variables are still substituted correctly
|
41
|
+
with patch.dict(os.environ, {"TEST_VAR": "test_value"}):
|
42
|
+
content = "This is a ${TEST_VAR} test"
|
43
|
+
result = _substitute_env_vars(content)
|
44
|
+
self.assertEqual(result, "This is a test_value test")
|
45
|
+
|
46
|
+
def test_substitute_env_vars_with_docker_secrets(self) -> None:
|
47
|
+
# Create a mock Docker secret
|
48
|
+
secret_path = os.path.join(self.secrets_dir, "db_password")
|
49
|
+
with open(secret_path, "w") as f:
|
50
|
+
f.write("secret_password")
|
51
|
+
|
52
|
+
# Mock the /run/secrets/ path
|
53
|
+
with patch("os.path.exists") as mock_exists:
|
54
|
+
mock_exists.return_value = True
|
55
|
+
with patch("builtins.open", mock_open(read_data="secret_password")):
|
56
|
+
content = "This is a ${DOCKER_SECRET:db_password} test"
|
57
|
+
result = _substitute_env_vars(content)
|
58
|
+
self.assertEqual(result, "This is a secret_password test")
|
59
|
+
|
60
|
+
def test_substitute_env_vars_with_missing_docker_secret(self) -> None:
|
61
|
+
# Test that a warning is logged when a Docker secret is missing
|
62
|
+
with patch("dbos._dbos_config.dbos_logger") as mock_logger:
|
63
|
+
content = "This is a ${DOCKER_SECRET:missing_secret} test"
|
64
|
+
result = _substitute_env_vars(content)
|
65
|
+
self.assertEqual(result, "This is a test")
|
66
|
+
mock_logger.warning.assert_called_once()
|
67
|
+
|
68
|
+
def test_substitute_env_vars_with_both_env_vars_and_docker_secrets(self) -> None:
|
69
|
+
# Test that both environment variables and Docker secrets are substituted
|
70
|
+
with patch.dict(os.environ, {"TEST_VAR": "test_value"}):
|
71
|
+
with patch("os.path.exists") as mock_exists:
|
72
|
+
mock_exists.return_value = True
|
73
|
+
with patch(
|
74
|
+
"builtins.open",
|
75
|
+
mock_open(read_data="secret_password"),
|
76
|
+
):
|
77
|
+
content = (
|
78
|
+
"This is a ${TEST_VAR} and ${DOCKER_SECRET:db_password} test"
|
79
|
+
)
|
80
|
+
result = _substitute_env_vars(content)
|
81
|
+
self.assertEqual(
|
82
|
+
result, "This is a test_value and secret_password test"
|
83
|
+
)
|
84
|
+
|
85
|
+
def test_substitute_env_vars_with_silent_mode(self) -> None:
|
86
|
+
# Test that no warning is logged when silent mode is enabled
|
87
|
+
with patch("dbos._dbos_config.dbos_logger") as mock_logger:
|
88
|
+
content = "This is a ${DOCKER_SECRET:missing_secret} test"
|
89
|
+
result = _substitute_env_vars(content, silent=True)
|
90
|
+
self.assertEqual(result, "This is a test")
|
91
|
+
mock_logger.warning.assert_not_called()
|
92
|
+
|
93
|
+
def test_load_config_with_docker_secrets(self) -> None:
|
94
|
+
# Create a mock configuration file with Docker secrets
|
95
|
+
config_content = """
|
96
|
+
name: test-app
|
97
|
+
database:
|
98
|
+
hostname: localhost
|
99
|
+
port: 5432
|
100
|
+
username: postgres
|
101
|
+
password: ${DOCKER_SECRET:db_password}
|
102
|
+
app_db_name: test_db
|
103
|
+
"""
|
104
|
+
|
105
|
+
# Mock the file open and read operations
|
106
|
+
mock_file = mock_open(read_data=config_content)
|
107
|
+
|
108
|
+
# Create a mock dictionary that would be returned by yaml.safe_load
|
109
|
+
mock_config_dict: ConfigFile = {
|
110
|
+
"name": "test-app",
|
111
|
+
"database": {
|
112
|
+
"hostname": "localhost",
|
113
|
+
"port": 5432,
|
114
|
+
"username": "postgres",
|
115
|
+
"password": "secret_password",
|
116
|
+
"app_db_name": "test_db",
|
117
|
+
},
|
118
|
+
}
|
119
|
+
|
120
|
+
# Mock the schema validation to always pass
|
121
|
+
with (
|
122
|
+
patch("builtins.open", mock_file),
|
123
|
+
patch("dbos._dbos_config.validate") as mock_validate,
|
124
|
+
patch("dbos._dbos_config.resources.files") as mock_resources,
|
125
|
+
patch("os.path.exists") as mock_exists,
|
126
|
+
patch(
|
127
|
+
"builtins.open", mock_open(read_data="secret_password"), create=True
|
128
|
+
) as mock_secret_file,
|
129
|
+
patch("yaml.safe_load") as mock_yaml_load,
|
130
|
+
):
|
131
|
+
|
132
|
+
# Set up the mocks
|
133
|
+
mock_exists.return_value = True
|
134
|
+
mock_resources.return_value.joinpath.return_value.open.return_value.__enter__.return_value.read.return_value = (
|
135
|
+
"{}"
|
136
|
+
)
|
137
|
+
mock_yaml_load.return_value = mock_config_dict
|
138
|
+
|
139
|
+
# Call the load_config function
|
140
|
+
config = load_config(run_process_config=False)
|
141
|
+
|
142
|
+
# Verify that the Docker secret was correctly substituted
|
143
|
+
self.assertEqual(config["database"]["password"], "secret_password")
|
144
|
+
|
145
|
+
# Verify that the schema validation was called
|
146
|
+
mock_validate.assert_called_once()
|
147
|
+
|
148
|
+
def test_load_config_with_docker_secrets_in_database_url(self) -> None:
|
149
|
+
# Create a mock configuration file with Docker secrets in the database URL
|
150
|
+
config_content = """
|
151
|
+
name: test-app
|
152
|
+
database_url: postgresql://postgres:${DOCKER_SECRET:db_password}@localhost:5432/test_db
|
153
|
+
"""
|
154
|
+
|
155
|
+
# Mock the file open and read operations
|
156
|
+
mock_file = mock_open(read_data=config_content)
|
157
|
+
|
158
|
+
# Create a mock dictionary that would be returned by yaml.safe_load
|
159
|
+
mock_config_dict: ConfigFile = {
|
160
|
+
"name": "test-app",
|
161
|
+
"database_url": "postgresql://postgres:secret_password@localhost:5432/test_db",
|
162
|
+
}
|
163
|
+
|
164
|
+
# Mock the schema validation to always pass
|
165
|
+
with (
|
166
|
+
patch("builtins.open", mock_file),
|
167
|
+
patch("dbos._dbos_config.validate") as mock_validate,
|
168
|
+
patch("dbos._dbos_config.resources.files") as mock_resources,
|
169
|
+
patch("os.path.exists") as mock_exists,
|
170
|
+
patch(
|
171
|
+
"builtins.open", mock_open(read_data="secret_password"), create=True
|
172
|
+
) as mock_secret_file,
|
173
|
+
patch("yaml.safe_load") as mock_yaml_load,
|
174
|
+
):
|
175
|
+
|
176
|
+
# Set up the mocks
|
177
|
+
mock_exists.return_value = True
|
178
|
+
mock_resources.return_value.joinpath.return_value.open.return_value.__enter__.return_value.read.return_value = (
|
179
|
+
"{}"
|
180
|
+
)
|
181
|
+
mock_yaml_load.return_value = mock_config_dict
|
182
|
+
|
183
|
+
# Call the load_config function
|
184
|
+
config = load_config(run_process_config=False)
|
185
|
+
|
186
|
+
# Verify that the Docker secret was correctly substituted in the database URL
|
187
|
+
self.assertEqual(
|
188
|
+
config["database_url"],
|
189
|
+
"postgresql://postgres:secret_password@localhost:5432/test_db",
|
190
|
+
)
|
191
|
+
|
192
|
+
# Verify that the schema validation was called
|
193
|
+
mock_validate.assert_called_once()
|
194
|
+
|
195
|
+
def test_load_config_with_docker_secrets_in_nested_config(self) -> None:
|
196
|
+
# Create a mock configuration file with Docker secrets in a nested configuration
|
197
|
+
config_content = """
|
198
|
+
name: test-app
|
199
|
+
telemetry:
|
200
|
+
OTLPExporter:
|
201
|
+
logsEndpoint: http://logs-collector:4317
|
202
|
+
tracesEndpoint: http://traces-collector:4317
|
203
|
+
logs:
|
204
|
+
logLevel: INFO
|
205
|
+
database:
|
206
|
+
hostname: localhost
|
207
|
+
port: 5432
|
208
|
+
username: postgres
|
209
|
+
password: ${DOCKER_SECRET:db_password}
|
210
|
+
app_db_name: test_db
|
211
|
+
nested:
|
212
|
+
secret: ${DOCKER_SECRET:nested_secret}
|
213
|
+
"""
|
214
|
+
|
215
|
+
# Mock the file open and read operations
|
216
|
+
mock_file = mock_open(read_data=config_content)
|
217
|
+
|
218
|
+
# Create a mock dictionary that would be returned by yaml.safe_load
|
219
|
+
mock_config_dict: ConfigFile = {
|
220
|
+
"name": "test-app",
|
221
|
+
"telemetry": {
|
222
|
+
"OTLPExporter": {
|
223
|
+
"logsEndpoint": "http://logs-collector:4317",
|
224
|
+
"tracesEndpoint": "http://traces-collector:4317",
|
225
|
+
},
|
226
|
+
"logs": {"logLevel": "INFO"},
|
227
|
+
},
|
228
|
+
"database": {
|
229
|
+
"hostname": "localhost",
|
230
|
+
"port": 5432,
|
231
|
+
"username": "postgres",
|
232
|
+
"password": "secret_password",
|
233
|
+
"app_db_name": "test_db",
|
234
|
+
"nested": {"secret": "secret_value"},
|
235
|
+
},
|
236
|
+
}
|
237
|
+
|
238
|
+
# Mock the schema validation to always pass
|
239
|
+
with (
|
240
|
+
patch("builtins.open", mock_file),
|
241
|
+
patch("dbos._dbos_config.validate") as mock_validate,
|
242
|
+
patch("dbos._dbos_config.resources.files") as mock_resources,
|
243
|
+
patch("os.path.exists") as mock_exists,
|
244
|
+
patch(
|
245
|
+
"builtins.open", mock_open(read_data="secret_value"), create=True
|
246
|
+
) as mock_secret_file,
|
247
|
+
patch("yaml.safe_load") as mock_yaml_load,
|
248
|
+
):
|
249
|
+
|
250
|
+
# Set up the mocks
|
251
|
+
mock_exists.return_value = True
|
252
|
+
mock_resources.return_value.joinpath.return_value.open.return_value.__enter__.return_value.read.return_value = (
|
253
|
+
"{}"
|
254
|
+
)
|
255
|
+
mock_yaml_load.return_value = mock_config_dict
|
256
|
+
|
257
|
+
# Call the load_config function
|
258
|
+
config = load_config(run_process_config=False)
|
259
|
+
|
260
|
+
# Verify that the Docker secrets were correctly substituted in the nested configuration
|
261
|
+
self.assertEqual(config["database"]["password"], "secret_password")
|
262
|
+
# Use cast to handle the nested field
|
263
|
+
database_config = cast(DatabaseConfig, config["database"])
|
264
|
+
self.assertEqual(database_config["nested"]["secret"], "secret_value")
|
265
|
+
|
266
|
+
# Verify that the schema validation was called
|
267
|
+
mock_validate.assert_called_once()
|
268
|
+
|
269
|
+
def test_load_config_with_docker_secrets_in_list(self) -> None:
|
270
|
+
# Create a mock configuration file with Docker secrets in a list
|
271
|
+
config_content = """
|
272
|
+
name: test-app
|
273
|
+
runtimeConfig:
|
274
|
+
setup:
|
275
|
+
- echo "Setting up environment"
|
276
|
+
- export API_KEY=${DOCKER_SECRET:api_key}
|
277
|
+
- export DB_PASSWORD=${DOCKER_SECRET:db_password}
|
278
|
+
"""
|
279
|
+
|
280
|
+
# Mock the file open and read operations
|
281
|
+
mock_file = mock_open(read_data=config_content)
|
282
|
+
|
283
|
+
# Create a mock dictionary that would be returned by yaml.safe_load
|
284
|
+
mock_config_dict: ConfigFile = {
|
285
|
+
"name": "test-app",
|
286
|
+
"runtimeConfig": {
|
287
|
+
"setup": [
|
288
|
+
'echo "Setting up environment"',
|
289
|
+
"export API_KEY=secret_value",
|
290
|
+
"export DB_PASSWORD=secret_value",
|
291
|
+
]
|
292
|
+
},
|
293
|
+
}
|
294
|
+
|
295
|
+
# Mock the schema validation to always pass
|
296
|
+
with (
|
297
|
+
patch("builtins.open", mock_file),
|
298
|
+
patch("dbos._dbos_config.validate") as mock_validate,
|
299
|
+
patch("dbos._dbos_config.resources.files") as mock_resources,
|
300
|
+
patch("os.path.exists") as mock_exists,
|
301
|
+
patch(
|
302
|
+
"builtins.open", mock_open(read_data="secret_value"), create=True
|
303
|
+
) as mock_secret_file,
|
304
|
+
patch("yaml.safe_load") as mock_yaml_load,
|
305
|
+
):
|
306
|
+
|
307
|
+
# Set up the mocks
|
308
|
+
mock_exists.return_value = True
|
309
|
+
mock_resources.return_value.joinpath.return_value.open.return_value.__enter__.return_value.read.return_value = (
|
310
|
+
"{}"
|
311
|
+
)
|
312
|
+
mock_yaml_load.return_value = mock_config_dict
|
313
|
+
|
314
|
+
# Call the load_config function
|
315
|
+
config = load_config(run_process_config=False)
|
316
|
+
|
317
|
+
# Verify that the Docker secrets were correctly substituted in the list
|
318
|
+
setup_commands = config["runtimeConfig"]["setup"]
|
319
|
+
if setup_commands is not None:
|
320
|
+
self.assertEqual(setup_commands[0], 'echo "Setting up environment"')
|
321
|
+
self.assertEqual(setup_commands[1], "export API_KEY=secret_value")
|
322
|
+
self.assertEqual(setup_commands[2], "export DB_PASSWORD=secret_value")
|
323
|
+
|
324
|
+
# Verify that the schema validation was called
|
325
|
+
mock_validate.assert_called_once()
|
326
|
+
|
327
|
+
def test_load_config_with_multiple_docker_secrets_in_string(self) -> None:
|
328
|
+
# Create a mock configuration file with multiple Docker secrets in a string
|
329
|
+
config_content = """
|
330
|
+
name: test-app
|
331
|
+
database:
|
332
|
+
hostname: ${DOCKER_SECRET:db_host}
|
333
|
+
port: ${DOCKER_SECRET:db_port}
|
334
|
+
username: ${DOCKER_SECRET:db_user}
|
335
|
+
password: ${DOCKER_SECRET:db_password}
|
336
|
+
app_db_name: ${DOCKER_SECRET:db_name}
|
337
|
+
"""
|
338
|
+
|
339
|
+
# Mock the file open and read operations
|
340
|
+
mock_file = mock_open(read_data=config_content)
|
341
|
+
|
342
|
+
# Create a mock dictionary that would be returned by yaml.safe_load
|
343
|
+
mock_config_dict: ConfigFile = {
|
344
|
+
"name": "test-app",
|
345
|
+
"database": {
|
346
|
+
"hostname": "host",
|
347
|
+
"port": 5432,
|
348
|
+
"username": "user",
|
349
|
+
"password": "pass",
|
350
|
+
"app_db_name": "db",
|
351
|
+
},
|
352
|
+
}
|
353
|
+
|
354
|
+
# Mock the schema validation to always pass
|
355
|
+
with (
|
356
|
+
patch("builtins.open", mock_file),
|
357
|
+
patch("dbos._dbos_config.validate") as mock_validate,
|
358
|
+
patch("dbos._dbos_config.resources.files") as mock_resources,
|
359
|
+
patch("os.path.exists") as mock_exists,
|
360
|
+
patch("yaml.safe_load") as mock_yaml_load,
|
361
|
+
):
|
362
|
+
|
363
|
+
# Set up the mocks
|
364
|
+
mock_exists.return_value = True
|
365
|
+
mock_resources.return_value.joinpath.return_value.open.return_value.__enter__.return_value.read.return_value = (
|
366
|
+
"{}"
|
367
|
+
)
|
368
|
+
mock_yaml_load.return_value = mock_config_dict
|
369
|
+
|
370
|
+
def mock_secret_open(*args: Any, **kwargs: Any) -> Any:
|
371
|
+
filepath = args[0]
|
372
|
+
if filepath == "dbos-config.yaml":
|
373
|
+
return mock_open(read_data=config_content).return_value
|
374
|
+
|
375
|
+
secret_name = filepath.split("/")[-1]
|
376
|
+
secret_values = {
|
377
|
+
"db_user": "user",
|
378
|
+
"db_password": "pass",
|
379
|
+
"db_host": "host",
|
380
|
+
"db_port": "5432",
|
381
|
+
"db_name": "db",
|
382
|
+
}
|
383
|
+
if secret_name in secret_values:
|
384
|
+
return mock_open(read_data=secret_values[secret_name]).return_value
|
385
|
+
raise FileNotFoundError(f"Mock file not found: {filepath}")
|
386
|
+
|
387
|
+
with patch("builtins.open", mock_secret_open, create=True):
|
388
|
+
# Call the load_config function
|
389
|
+
config = load_config(run_process_config=False)
|
390
|
+
|
391
|
+
# Verify that all Docker secrets were correctly substituted in the string
|
392
|
+
self.assertEqual(config["database"]["hostname"], "host")
|
393
|
+
self.assertEqual(config["database"]["port"], 5432)
|
394
|
+
self.assertEqual(config["database"]["username"], "user")
|
395
|
+
self.assertEqual(config["database"]["password"], "pass")
|
396
|
+
self.assertEqual(config["database"]["app_db_name"], "db")
|
397
|
+
|
398
|
+
# Verify that the schema validation was called
|
399
|
+
mock_validate.assert_called_once()
|
400
|
+
|
401
|
+
def test_load_config_without_docker_secrets(self) -> None:
|
402
|
+
# Create a mock configuration file without Docker secrets
|
403
|
+
config_content = """
|
404
|
+
name: test-app
|
405
|
+
database:
|
406
|
+
hostname: localhost
|
407
|
+
port: 5432
|
408
|
+
username: postgres
|
409
|
+
password: plain_password
|
410
|
+
app_db_name: test_db
|
411
|
+
"""
|
412
|
+
|
413
|
+
# Mock the file open and read operations
|
414
|
+
mock_file = mock_open(read_data=config_content)
|
415
|
+
|
416
|
+
# Create a mock dictionary that would be returned by yaml.safe_load
|
417
|
+
mock_config_dict: ConfigFile = {
|
418
|
+
"name": "test-app",
|
419
|
+
"database": {
|
420
|
+
"hostname": "localhost",
|
421
|
+
"port": 5432,
|
422
|
+
"username": "postgres",
|
423
|
+
"password": "plain_password",
|
424
|
+
"app_db_name": "test_db",
|
425
|
+
},
|
426
|
+
}
|
427
|
+
|
428
|
+
# Mock the schema validation to always pass
|
429
|
+
with (
|
430
|
+
patch("builtins.open", mock_file),
|
431
|
+
patch("dbos._dbos_config.validate") as mock_validate,
|
432
|
+
patch("dbos._dbos_config.resources.files") as mock_resources,
|
433
|
+
patch("yaml.safe_load") as mock_yaml_load,
|
434
|
+
):
|
435
|
+
|
436
|
+
# Set up the mocks
|
437
|
+
mock_resources.return_value.joinpath.return_value.open.return_value.__enter__.return_value.read.return_value = (
|
438
|
+
"{}"
|
439
|
+
)
|
440
|
+
mock_yaml_load.return_value = mock_config_dict
|
441
|
+
|
442
|
+
# Call the load_config function
|
443
|
+
config = load_config(run_process_config=False)
|
444
|
+
|
445
|
+
# Verify that the configuration was loaded correctly without any substitutions
|
446
|
+
self.assertEqual(config["database"]["password"], "plain_password")
|
447
|
+
|
448
|
+
# Verify that the schema validation was called
|
449
|
+
mock_validate.assert_called_once()
|
450
|
+
|
451
|
+
def test_load_config_with_mixed_env_vars_and_docker_secrets(self) -> None:
|
452
|
+
# Create a mock configuration file with both environment variables and Docker secrets
|
453
|
+
config_content = """
|
454
|
+
name: test-app
|
455
|
+
database:
|
456
|
+
hostname: ${DB_HOST}
|
457
|
+
port: ${DB_PORT}
|
458
|
+
username: ${DB_USER}
|
459
|
+
password: ${DOCKER_SECRET:db_password}
|
460
|
+
app_db_name: ${DB_NAME}
|
461
|
+
"""
|
462
|
+
|
463
|
+
# Mock the file open and read operations
|
464
|
+
mock_file = mock_open(read_data=config_content)
|
465
|
+
|
466
|
+
# Create a mock dictionary that would be returned by yaml.safe_load
|
467
|
+
mock_config_dict: ConfigFile = {
|
468
|
+
"name": "test-app",
|
469
|
+
"database": {
|
470
|
+
"hostname": "localhost",
|
471
|
+
"port": 5432,
|
472
|
+
"username": "postgres",
|
473
|
+
"password": "secret_password",
|
474
|
+
"app_db_name": "test_db",
|
475
|
+
},
|
476
|
+
}
|
477
|
+
|
478
|
+
# Mock the schema validation to always pass
|
479
|
+
with (
|
480
|
+
patch("builtins.open", mock_file),
|
481
|
+
patch("dbos._dbos_config.validate") as mock_validate,
|
482
|
+
patch("dbos._dbos_config.resources.files") as mock_resources,
|
483
|
+
patch("os.path.exists") as mock_exists,
|
484
|
+
patch(
|
485
|
+
"builtins.open", mock_open(read_data="secret_password"), create=True
|
486
|
+
) as mock_secret_file,
|
487
|
+
patch("yaml.safe_load") as mock_yaml_load,
|
488
|
+
patch.dict(
|
489
|
+
os.environ,
|
490
|
+
{
|
491
|
+
"DB_HOST": "localhost",
|
492
|
+
"DB_PORT": "5432",
|
493
|
+
"DB_USER": "postgres",
|
494
|
+
"DB_NAME": "test_db",
|
495
|
+
},
|
496
|
+
),
|
497
|
+
):
|
498
|
+
|
499
|
+
# Set up the mocks
|
500
|
+
mock_exists.return_value = True
|
501
|
+
mock_resources.return_value.joinpath.return_value.open.return_value.__enter__.return_value.read.return_value = (
|
502
|
+
"{}"
|
503
|
+
)
|
504
|
+
mock_yaml_load.return_value = mock_config_dict
|
505
|
+
|
506
|
+
# Call the load_config function
|
507
|
+
config = load_config(run_process_config=False)
|
508
|
+
|
509
|
+
# Verify that both environment variables and Docker secrets were correctly substituted
|
510
|
+
self.assertEqual(config["database"]["hostname"], "localhost")
|
511
|
+
self.assertEqual(config["database"]["port"], 5432)
|
512
|
+
self.assertEqual(config["database"]["username"], "postgres")
|
513
|
+
self.assertEqual(config["database"]["password"], "secret_password")
|
514
|
+
self.assertEqual(config["database"]["app_db_name"], "test_db")
|
515
|
+
|
516
|
+
# Verify that the schema validation was called
|
517
|
+
mock_validate.assert_called_once()
|
518
|
+
|
519
|
+
|
520
|
+
if __name__ == "__main__":
|
521
|
+
unittest.main()
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py
RENAMED
File without changes
|
File without changes
|
{dbos-0.25.0a16 → dbos-0.26.0a1}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|