dbos 0.26.0a0__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.
Files changed (104) hide show
  1. {dbos-0.26.0a0 → dbos-0.26.0a1}/PKG-INFO +1 -1
  2. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_dbos_config.py +30 -3
  3. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/dbos-config.schema.json +1 -1
  4. {dbos-0.26.0a0 → dbos-0.26.0a1}/pyproject.toml +1 -1
  5. dbos-0.26.0a1/tests/test_docker_secrets.py +521 -0
  6. {dbos-0.26.0a0 → dbos-0.26.0a1}/LICENSE +0 -0
  7. {dbos-0.26.0a0 → dbos-0.26.0a1}/README.md +0 -0
  8. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/__init__.py +0 -0
  9. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/__main__.py +0 -0
  10. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_admin_server.py +0 -0
  11. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_app_db.py +0 -0
  12. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_classproperty.py +0 -0
  13. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_client.py +0 -0
  14. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_cloudutils/authentication.py +0 -0
  15. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_cloudutils/cloudutils.py +0 -0
  16. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_cloudutils/databases.py +0 -0
  17. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_conductor/conductor.py +0 -0
  18. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_conductor/protocol.py +0 -0
  19. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_context.py +0 -0
  20. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_core.py +0 -0
  21. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_croniter.py +0 -0
  22. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_db_wizard.py +0 -0
  23. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_dbos.py +0 -0
  24. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_debug.py +0 -0
  25. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_error.py +0 -0
  26. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_fastapi.py +0 -0
  27. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_flask.py +0 -0
  28. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_kafka.py +0 -0
  29. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_kafka_message.py +0 -0
  30. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_logger.py +0 -0
  31. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_migrations/env.py +0 -0
  32. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_migrations/script.py.mako +0 -0
  33. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
  34. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
  35. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
  36. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
  37. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
  38. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
  39. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
  40. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_migrations/versions/f4b9b32ba814_functionname_childid_op_outputs.py +0 -0
  41. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_outcome.py +0 -0
  42. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_queue.py +0 -0
  43. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_recovery.py +0 -0
  44. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_registrations.py +0 -0
  45. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_request.py +0 -0
  46. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_roles.py +0 -0
  47. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_scheduler.py +0 -0
  48. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_schemas/__init__.py +0 -0
  49. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_schemas/application_database.py +0 -0
  50. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_schemas/system_database.py +0 -0
  51. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_serialization.py +0 -0
  52. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_sys_db.py +0 -0
  53. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_templates/dbos-db-starter/README.md +0 -0
  54. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
  55. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_templates/dbos-db-starter/__package/main.py +0 -0
  56. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
  57. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
  58. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
  59. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
  60. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
  61. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
  62. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
  63. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_tracer.py +0 -0
  64. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_utils.py +0 -0
  65. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/_workflow_commands.py +0 -0
  66. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/cli/_github_init.py +0 -0
  67. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/cli/_template_init.py +0 -0
  68. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/cli/cli.py +0 -0
  69. {dbos-0.26.0a0 → dbos-0.26.0a1}/dbos/py.typed +0 -0
  70. {dbos-0.26.0a0 → dbos-0.26.0a1}/tests/__init__.py +0 -0
  71. {dbos-0.26.0a0 → dbos-0.26.0a1}/tests/atexit_no_ctor.py +0 -0
  72. {dbos-0.26.0a0 → dbos-0.26.0a1}/tests/atexit_no_launch.py +0 -0
  73. {dbos-0.26.0a0 → dbos-0.26.0a1}/tests/classdefs.py +0 -0
  74. {dbos-0.26.0a0 → dbos-0.26.0a1}/tests/client_collateral.py +0 -0
  75. {dbos-0.26.0a0 → dbos-0.26.0a1}/tests/client_worker.py +0 -0
  76. {dbos-0.26.0a0 → dbos-0.26.0a1}/tests/conftest.py +0 -0
  77. {dbos-0.26.0a0 → dbos-0.26.0a1}/tests/more_classdefs.py +0 -0
  78. {dbos-0.26.0a0 → dbos-0.26.0a1}/tests/queuedworkflow.py +0 -0
  79. {dbos-0.26.0a0 → dbos-0.26.0a1}/tests/test_admin_server.py +0 -0
  80. {dbos-0.26.0a0 → dbos-0.26.0a1}/tests/test_async.py +0 -0
  81. {dbos-0.26.0a0 → dbos-0.26.0a1}/tests/test_classdecorators.py +0 -0
  82. {dbos-0.26.0a0 → dbos-0.26.0a1}/tests/test_client.py +0 -0
  83. {dbos-0.26.0a0 → dbos-0.26.0a1}/tests/test_concurrency.py +0 -0
  84. {dbos-0.26.0a0 → dbos-0.26.0a1}/tests/test_config.py +0 -0
  85. {dbos-0.26.0a0 → dbos-0.26.0a1}/tests/test_croniter.py +0 -0
  86. {dbos-0.26.0a0 → dbos-0.26.0a1}/tests/test_dbos.py +0 -0
  87. {dbos-0.26.0a0 → dbos-0.26.0a1}/tests/test_dbwizard.py +0 -0
  88. {dbos-0.26.0a0 → dbos-0.26.0a1}/tests/test_debug.py +0 -0
  89. {dbos-0.26.0a0 → dbos-0.26.0a1}/tests/test_failures.py +0 -0
  90. {dbos-0.26.0a0 → dbos-0.26.0a1}/tests/test_fastapi.py +0 -0
  91. {dbos-0.26.0a0 → dbos-0.26.0a1}/tests/test_fastapi_roles.py +0 -0
  92. {dbos-0.26.0a0 → dbos-0.26.0a1}/tests/test_flask.py +0 -0
  93. {dbos-0.26.0a0 → dbos-0.26.0a1}/tests/test_kafka.py +0 -0
  94. {dbos-0.26.0a0 → dbos-0.26.0a1}/tests/test_outcome.py +0 -0
  95. {dbos-0.26.0a0 → dbos-0.26.0a1}/tests/test_package.py +0 -0
  96. {dbos-0.26.0a0 → dbos-0.26.0a1}/tests/test_queue.py +0 -0
  97. {dbos-0.26.0a0 → dbos-0.26.0a1}/tests/test_scheduler.py +0 -0
  98. {dbos-0.26.0a0 → dbos-0.26.0a1}/tests/test_schema_migration.py +0 -0
  99. {dbos-0.26.0a0 → dbos-0.26.0a1}/tests/test_singleton.py +0 -0
  100. {dbos-0.26.0a0 → dbos-0.26.0a1}/tests/test_spans.py +0 -0
  101. {dbos-0.26.0a0 → dbos-0.26.0a1}/tests/test_sqlalchemy.py +0 -0
  102. {dbos-0.26.0a0 → dbos-0.26.0a1}/tests/test_workflow_cancel.py +0 -0
  103. {dbos-0.26.0a0 → dbos-0.26.0a1}/tests/test_workflow_cmds.py +0 -0
  104. {dbos-0.26.0a0 → dbos-0.26.0a1}/version/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbos
3
- Version: 0.26.0a0
3
+ Version: 0.26.0a1
4
4
  Summary: Ultra-lightweight durable execution in Python
5
5
  Author-Email: "DBOS, Inc." <contact@dbos.dev>
6
6
  License: MIT
@@ -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
- def replace_func(match: re.Match[str]) -> str:
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
- return re.sub(regex, replace_func, content)
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",
@@ -28,7 +28,7 @@ dependencies = [
28
28
  ]
29
29
  requires-python = ">=3.9"
30
30
  readme = "README.md"
31
- version = "0.26.0a0"
31
+ version = "0.26.0a1"
32
32
 
33
33
  [project.license]
34
34
  text = "MIT"
@@ -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
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