devservices 1.2.0__tar.gz → 1.2.1__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.
- {devservices-1.2.0 → devservices-1.2.1}/PKG-INFO +1 -1
- {devservices-1.2.0 → devservices-1.2.1}/README.md +3 -1
- {devservices-1.2.0 → devservices-1.2.1}/devservices/commands/down.py +24 -10
- {devservices-1.2.0 → devservices-1.2.1}/devservices/commands/foreground.py +8 -7
- {devservices-1.2.0 → devservices-1.2.1}/devservices/commands/logs.py +3 -5
- {devservices-1.2.0 → devservices-1.2.1}/devservices/commands/serve.py +17 -9
- {devservices-1.2.0 → devservices-1.2.1}/devservices/commands/status.py +10 -8
- {devservices-1.2.0 → devservices-1.2.1}/devservices/commands/up.py +23 -10
- {devservices-1.2.0 → devservices-1.2.1}/devservices/configs/service_config.py +14 -10
- {devservices-1.2.0 → devservices-1.2.1}/devservices/constants.py +0 -1
- {devservices-1.2.0 → devservices-1.2.1}/devservices/utils/supervisor.py +73 -12
- {devservices-1.2.0 → devservices-1.2.1}/devservices.egg-info/PKG-INFO +1 -1
- {devservices-1.2.0 → devservices-1.2.1}/pyproject.toml +1 -1
- {devservices-1.2.0 → devservices-1.2.1}/testing/utils.py +0 -9
- {devservices-1.2.0 → devservices-1.2.1}/tests/commands/test_down.py +24 -59
- {devservices-1.2.0 → devservices-1.2.1}/tests/commands/test_foreground.py +59 -95
- {devservices-1.2.0 → devservices-1.2.1}/tests/commands/test_list_services.py +1 -1
- {devservices-1.2.0 → devservices-1.2.1}/tests/commands/test_logs.py +29 -45
- {devservices-1.2.0 → devservices-1.2.1}/tests/commands/test_serve.py +14 -19
- {devservices-1.2.0 → devservices-1.2.1}/tests/commands/test_status.py +39 -38
- {devservices-1.2.0 → devservices-1.2.1}/tests/commands/test_up.py +48 -35
- {devservices-1.2.0 → devservices-1.2.1}/tests/configs/test_service_config.py +54 -19
- {devservices-1.2.0 → devservices-1.2.1}/tests/utils/test_supervisor.py +103 -10
- {devservices-1.2.0 → devservices-1.2.1}/LICENSE.md +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/devservices/__init__.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/devservices/commands/__init__.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/devservices/commands/list_dependencies.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/devservices/commands/list_services.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/devservices/commands/purge.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/devservices/commands/reset.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/devservices/commands/toggle.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/devservices/commands/update.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/devservices/exceptions.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/devservices/main.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/devservices/utils/__init__.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/devservices/utils/check_for_update.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/devservices/utils/console.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/devservices/utils/dependencies.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/devservices/utils/devenv.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/devservices/utils/docker.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/devservices/utils/docker_compose.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/devservices/utils/file_lock.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/devservices/utils/git.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/devservices/utils/install_binary.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/devservices/utils/services.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/devservices/utils/state.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/devservices.egg-info/SOURCES.txt +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/devservices.egg-info/dependency_links.txt +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/devservices.egg-info/entry_points.txt +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/devservices.egg-info/requires.txt +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/devservices.egg-info/top_level.txt +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/setup.cfg +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/testing/__init__.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/tests/__init__.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/tests/commands/test_list_dependencies.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/tests/commands/test_purge.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/tests/commands/test_reset.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/tests/commands/test_toggle.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/tests/commands/test_update.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/tests/conftest.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/tests/utils/test_check_for_update.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/tests/utils/test_dependencies.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/tests/utils/test_docker.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/tests/utils/test_docker_compose.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/tests/utils/test_git.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/tests/utils/test_install_binary.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/tests/utils/test_services.py +0 -0
- {devservices-1.2.0 → devservices-1.2.1}/tests/utils/test_state.py +0 -0
|
@@ -23,6 +23,8 @@ NOTE: service-name is an optional parameter. If not provided, devservices will a
|
|
|
23
23
|
- `devservices update` Update devservices to the latest version.
|
|
24
24
|
- `devservices purge`: Purge the local devservices cache.
|
|
25
25
|
- `devservices toggle <service-name>`: Toggle the runtime for a service between containerized and local.
|
|
26
|
+
- `devservices serve`: Run a service's devserver.
|
|
27
|
+
- `devservices foreground <process-name>`: Foreground a process that is currently running in the background. This enables interactive debugging.
|
|
26
28
|
|
|
27
29
|
## Installation
|
|
28
30
|
|
|
@@ -31,7 +33,7 @@ NOTE: service-name is an optional parameter. If not provided, devservices will a
|
|
|
31
33
|
The recommended way to install devservices is through a virtualenv in the requirements.txt. Once that is installed and a devservices config file is added, you should be able to run `devservices up` to begin local development.
|
|
32
34
|
|
|
33
35
|
```
|
|
34
|
-
devservices==1.2.
|
|
36
|
+
devservices==1.2.1
|
|
35
37
|
```
|
|
36
38
|
|
|
37
39
|
### 2. Add devservices config files
|
|
@@ -15,12 +15,12 @@ from devservices.constants import DependencyType
|
|
|
15
15
|
from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR
|
|
16
16
|
from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
|
|
17
17
|
from devservices.constants import DEVSERVICES_DIR_NAME
|
|
18
|
-
from devservices.constants import PROGRAMS_CONF_FILE_NAME
|
|
19
18
|
from devservices.exceptions import ConfigError
|
|
20
19
|
from devservices.exceptions import ConfigNotFoundError
|
|
21
20
|
from devservices.exceptions import DependencyError
|
|
22
21
|
from devservices.exceptions import DockerComposeError
|
|
23
22
|
from devservices.exceptions import ServiceNotFoundError
|
|
23
|
+
from devservices.exceptions import SupervisorConfigError
|
|
24
24
|
from devservices.exceptions import SupervisorError
|
|
25
25
|
from devservices.utils.console import Console
|
|
26
26
|
from devservices.utils.console import Status
|
|
@@ -305,22 +305,36 @@ def _bring_down_dependency(
|
|
|
305
305
|
return run_cmd(cmd.full_command, current_env)
|
|
306
306
|
|
|
307
307
|
|
|
308
|
+
def _stop_supervisor_program(
|
|
309
|
+
manager: SupervisorManager, program: str, status: Status
|
|
310
|
+
) -> None:
|
|
311
|
+
"""Stop a single supervisor program."""
|
|
312
|
+
status.info(f"Stopping {program}")
|
|
313
|
+
manager.stop_process(program)
|
|
314
|
+
|
|
315
|
+
|
|
308
316
|
def bring_down_supervisor_programs(
|
|
309
317
|
supervisor_programs: list[str], service: Service, status: Status
|
|
310
318
|
) -> None:
|
|
311
319
|
if len(supervisor_programs) == 0:
|
|
312
320
|
return
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
manager = SupervisorManager(
|
|
317
|
-
programs_config_path,
|
|
318
|
-
service_name=service.name,
|
|
321
|
+
|
|
322
|
+
config_file_path = os.path.join(
|
|
323
|
+
service.repo_path, DEVSERVICES_DIR_NAME, CONFIG_FILE_NAME
|
|
319
324
|
)
|
|
320
325
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
326
|
+
try:
|
|
327
|
+
manager = SupervisorManager(service.name, config_file_path)
|
|
328
|
+
except SupervisorConfigError:
|
|
329
|
+
raise
|
|
330
|
+
|
|
331
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
332
|
+
futures = [
|
|
333
|
+
executor.submit(_stop_supervisor_program, manager, program, status)
|
|
334
|
+
for program in supervisor_programs
|
|
335
|
+
]
|
|
336
|
+
for future in concurrent.futures.as_completed(futures):
|
|
337
|
+
_ = future.result()
|
|
324
338
|
|
|
325
339
|
status.info("Stopping supervisor daemon")
|
|
326
340
|
manager.stop_supervisor_daemon()
|
|
@@ -9,9 +9,9 @@ from argparse import Namespace
|
|
|
9
9
|
|
|
10
10
|
from sentry_sdk import capture_exception
|
|
11
11
|
|
|
12
|
+
from devservices.constants import CONFIG_FILE_NAME
|
|
12
13
|
from devservices.constants import DependencyType
|
|
13
14
|
from devservices.constants import DEVSERVICES_DIR_NAME
|
|
14
|
-
from devservices.constants import PROGRAMS_CONF_FILE_NAME
|
|
15
15
|
from devservices.exceptions import ConfigError
|
|
16
16
|
from devservices.exceptions import ConfigNotFoundError
|
|
17
17
|
from devservices.exceptions import ServiceNotFoundError
|
|
@@ -92,14 +92,15 @@ def foreground(args: Namespace) -> None:
|
|
|
92
92
|
)
|
|
93
93
|
return
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
service.repo_path,
|
|
95
|
+
config_file_path = os.path.join(
|
|
96
|
+
service.repo_path, DEVSERVICES_DIR_NAME, CONFIG_FILE_NAME
|
|
97
97
|
)
|
|
98
98
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
try:
|
|
100
|
+
manager = SupervisorManager(service.name, config_file_path)
|
|
101
|
+
except SupervisorConfigError as e:
|
|
102
|
+
capture_exception(e, level="info")
|
|
103
|
+
return
|
|
103
104
|
|
|
104
105
|
try:
|
|
105
106
|
program_command = manager.get_program_command(program_name)
|
|
@@ -16,7 +16,6 @@ from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR
|
|
|
16
16
|
from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
|
|
17
17
|
from devservices.constants import DEVSERVICES_DIR_NAME
|
|
18
18
|
from devservices.constants import MAX_LOG_LINES
|
|
19
|
-
from devservices.constants import PROGRAMS_CONF_FILE_NAME
|
|
20
19
|
from devservices.exceptions import ConfigError
|
|
21
20
|
from devservices.exceptions import ConfigNotFoundError
|
|
22
21
|
from devservices.exceptions import DependencyError
|
|
@@ -178,12 +177,11 @@ def _supervisor_logs(
|
|
|
178
177
|
|
|
179
178
|
supervisor_logs: dict[str, str] = {}
|
|
180
179
|
|
|
181
|
-
|
|
182
|
-
service.repo_path, DEVSERVICES_DIR_NAME,
|
|
180
|
+
config_file_path = os.path.join(
|
|
181
|
+
service.repo_path, DEVSERVICES_DIR_NAME, CONFIG_FILE_NAME
|
|
183
182
|
)
|
|
184
|
-
|
|
185
183
|
try:
|
|
186
|
-
manager = SupervisorManager(
|
|
184
|
+
manager = SupervisorManager(service.name, config_file_path)
|
|
187
185
|
except SupervisorConfigError as e:
|
|
188
186
|
capture_exception(e)
|
|
189
187
|
return supervisor_logs
|
|
@@ -9,8 +9,8 @@ from argparse import Namespace
|
|
|
9
9
|
|
|
10
10
|
from sentry_sdk import capture_exception
|
|
11
11
|
|
|
12
|
+
from devservices.constants import CONFIG_FILE_NAME
|
|
12
13
|
from devservices.constants import DEVSERVICES_DIR_NAME
|
|
13
|
-
from devservices.constants import PROGRAMS_CONF_FILE_NAME
|
|
14
14
|
from devservices.exceptions import ConfigError
|
|
15
15
|
from devservices.exceptions import ConfigNotFoundError
|
|
16
16
|
from devservices.exceptions import SupervisorConfigError
|
|
@@ -46,16 +46,24 @@ def serve(args: Namespace) -> None:
|
|
|
46
46
|
console.failure(str(e))
|
|
47
47
|
exit(1)
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
service.repo_path,
|
|
49
|
+
config_file_path = os.path.join(
|
|
50
|
+
service.repo_path, DEVSERVICES_DIR_NAME, CONFIG_FILE_NAME
|
|
51
51
|
)
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
manager = SupervisorManager(service.name, config_file_path)
|
|
55
|
+
except SupervisorConfigError as e:
|
|
56
|
+
capture_exception(e, level="info")
|
|
57
|
+
console.failure(
|
|
58
|
+
f"Unable to bring up devserver due to supervisor config error: {str(e)}"
|
|
59
|
+
)
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
if not manager.has_programs:
|
|
63
|
+
console.failure(
|
|
64
|
+
"No programs found in config. Please add the devserver in the `x-programs` block to your config.yml"
|
|
65
|
+
)
|
|
54
66
|
return
|
|
55
|
-
manager = SupervisorManager(
|
|
56
|
-
programs_config_path,
|
|
57
|
-
service_name=service.name,
|
|
58
|
-
)
|
|
59
67
|
|
|
60
68
|
try:
|
|
61
69
|
devserver_command = manager.get_program_command("devserver")
|
|
@@ -20,12 +20,12 @@ from devservices.constants import DependencyType
|
|
|
20
20
|
from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR
|
|
21
21
|
from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
|
|
22
22
|
from devservices.constants import DEVSERVICES_DIR_NAME
|
|
23
|
-
from devservices.constants import PROGRAMS_CONF_FILE_NAME
|
|
24
23
|
from devservices.exceptions import ConfigError
|
|
25
24
|
from devservices.exceptions import ConfigNotFoundError
|
|
26
25
|
from devservices.exceptions import DependencyError
|
|
27
26
|
from devservices.exceptions import DockerComposeError
|
|
28
27
|
from devservices.exceptions import ServiceNotFoundError
|
|
28
|
+
from devservices.exceptions import SupervisorConfigError
|
|
29
29
|
from devservices.utils.console import Console
|
|
30
30
|
from devservices.utils.dependencies import construct_dependency_graph
|
|
31
31
|
from devservices.utils.dependencies import DependencyGraph
|
|
@@ -103,16 +103,18 @@ def status(args: Namespace) -> None:
|
|
|
103
103
|
console.warning(f"Status unavailable. {service.name} is not running standalone")
|
|
104
104
|
return # Since exit(0) is captured as an internal_error by sentry
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
service.repo_path,
|
|
106
|
+
config_file_path = os.path.join(
|
|
107
|
+
service.repo_path, DEVSERVICES_DIR_NAME, CONFIG_FILE_NAME
|
|
108
108
|
)
|
|
109
109
|
process_statuses = {}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
service.name,
|
|
114
|
-
)
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
supervisor_manager = SupervisorManager(service.name, config_file_path)
|
|
115
113
|
process_statuses = supervisor_manager.get_all_process_info()
|
|
114
|
+
except SupervisorConfigError as e:
|
|
115
|
+
capture_exception(e)
|
|
116
|
+
console.failure(str(e))
|
|
117
|
+
exit(1)
|
|
116
118
|
|
|
117
119
|
try:
|
|
118
120
|
status_tree = get_status_for_service(service, process_statuses)
|
|
@@ -17,7 +17,6 @@ from devservices.constants import DependencyType
|
|
|
17
17
|
from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR
|
|
18
18
|
from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
|
|
19
19
|
from devservices.constants import DEVSERVICES_DIR_NAME
|
|
20
|
-
from devservices.constants import PROGRAMS_CONF_FILE_NAME
|
|
21
20
|
from devservices.exceptions import ConfigError
|
|
22
21
|
from devservices.exceptions import ConfigNotFoundError
|
|
23
22
|
from devservices.exceptions import ContainerHealthcheckFailedError
|
|
@@ -25,6 +24,7 @@ from devservices.exceptions import DependencyError
|
|
|
25
24
|
from devservices.exceptions import DockerComposeError
|
|
26
25
|
from devservices.exceptions import ModeDoesNotExistError
|
|
27
26
|
from devservices.exceptions import ServiceNotFoundError
|
|
27
|
+
from devservices.exceptions import SupervisorConfigError
|
|
28
28
|
from devservices.exceptions import SupervisorError
|
|
29
29
|
from devservices.utils.console import Console
|
|
30
30
|
from devservices.utils.console import Status
|
|
@@ -381,6 +381,14 @@ def bring_up_docker_compose_services(
|
|
|
381
381
|
exit(1)
|
|
382
382
|
|
|
383
383
|
|
|
384
|
+
def _start_supervisor_program(
|
|
385
|
+
manager: SupervisorManager, program: str, status: Status
|
|
386
|
+
) -> None:
|
|
387
|
+
"""Start a single supervisor program."""
|
|
388
|
+
status.info(f"Starting {program}")
|
|
389
|
+
manager.start_process(program)
|
|
390
|
+
|
|
391
|
+
|
|
384
392
|
def bring_up_supervisor_programs(
|
|
385
393
|
service: Service, supervisor_programs: list[str], status: Status
|
|
386
394
|
) -> None:
|
|
@@ -391,18 +399,23 @@ def bring_up_supervisor_programs(
|
|
|
391
399
|
f"Cannot bring up supervisor programs from outside the service repository. Please run the command from the service repository ({service.repo_path})"
|
|
392
400
|
)
|
|
393
401
|
return
|
|
394
|
-
programs_config_path = os.path.join(
|
|
395
|
-
service.repo_path, f"{DEVSERVICES_DIR_NAME}/{PROGRAMS_CONF_FILE_NAME}"
|
|
396
|
-
)
|
|
397
402
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
service_name=service.name,
|
|
403
|
+
config_file_path = os.path.join(
|
|
404
|
+
service.repo_path, DEVSERVICES_DIR_NAME, CONFIG_FILE_NAME
|
|
401
405
|
)
|
|
402
406
|
|
|
407
|
+
try:
|
|
408
|
+
manager = SupervisorManager(service.name, config_file_path)
|
|
409
|
+
except SupervisorConfigError:
|
|
410
|
+
raise
|
|
411
|
+
|
|
403
412
|
status.info("Starting supervisor daemon")
|
|
404
413
|
manager.start_supervisor_daemon()
|
|
405
414
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
415
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
416
|
+
futures = [
|
|
417
|
+
executor.submit(_start_supervisor_program, manager, program, status)
|
|
418
|
+
for program in supervisor_programs
|
|
419
|
+
]
|
|
420
|
+
for future in concurrent.futures.as_completed(futures):
|
|
421
|
+
_ = future.result()
|
|
@@ -10,10 +10,10 @@ from supervisor.options import ServerOptions
|
|
|
10
10
|
from devservices.constants import CONFIG_FILE_NAME
|
|
11
11
|
from devservices.constants import DependencyType
|
|
12
12
|
from devservices.constants import DEVSERVICES_DIR_NAME
|
|
13
|
-
from devservices.constants import PROGRAMS_CONF_FILE_NAME
|
|
14
13
|
from devservices.exceptions import ConfigNotFoundError
|
|
15
14
|
from devservices.exceptions import ConfigParseError
|
|
16
15
|
from devservices.exceptions import ConfigValidationError
|
|
16
|
+
from devservices.utils.supervisor import ProgramData
|
|
17
17
|
from devservices.utils.supervisor import SupervisorManager
|
|
18
18
|
|
|
19
19
|
VALID_VERSIONS = [0.1]
|
|
@@ -91,8 +91,10 @@ def load_service_config_from_file(repo_path: str) -> ServiceConfig:
|
|
|
91
91
|
|
|
92
92
|
docker_compose_services = config.get("services", {}).keys()
|
|
93
93
|
|
|
94
|
-
supervisor_programs =
|
|
95
|
-
|
|
94
|
+
supervisor_programs = load_supervisor_programs_from_programs_data(
|
|
95
|
+
config_path,
|
|
96
|
+
service_config_data.get("service_name"),
|
|
97
|
+
config.get("x-programs", {}),
|
|
96
98
|
)
|
|
97
99
|
|
|
98
100
|
valid_dependency_keys = {field.name for field in fields(Dependency)}
|
|
@@ -113,7 +115,7 @@ def load_service_config_from_file(repo_path: str) -> ServiceConfig:
|
|
|
113
115
|
dependency_type = DependencyType.COMPOSE
|
|
114
116
|
else:
|
|
115
117
|
raise ConfigValidationError(
|
|
116
|
-
f"Dependency '{key}' is not remote but is not defined in docker-compose services or programs
|
|
118
|
+
f"Dependency '{key}' is not remote but is not defined in docker-compose services or x-programs"
|
|
117
119
|
)
|
|
118
120
|
else:
|
|
119
121
|
dependency_type = DependencyType.SERVICE
|
|
@@ -142,13 +144,15 @@ def load_service_config_from_file(repo_path: str) -> ServiceConfig:
|
|
|
142
144
|
return service_config
|
|
143
145
|
|
|
144
146
|
|
|
145
|
-
def
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if not os.path.exists(programs_config_path):
|
|
147
|
+
def load_supervisor_programs_from_programs_data(
|
|
148
|
+
service_config_path: str, service_name: str, programs_data: ProgramData
|
|
149
|
+
) -> set[str]:
|
|
150
|
+
if not programs_data:
|
|
150
151
|
return set()
|
|
151
|
-
|
|
152
|
+
|
|
153
|
+
manager = SupervisorManager(
|
|
154
|
+
service_name=service_name, service_config_path=service_config_path
|
|
155
|
+
)
|
|
152
156
|
opts = ServerOptions()
|
|
153
157
|
opts.configfile = manager.config_file_path
|
|
154
158
|
opts.process_config()
|
|
@@ -25,7 +25,6 @@ class DependencyType(StrEnum):
|
|
|
25
25
|
MINIMUM_DOCKER_COMPOSE_VERSION = "2.29.7"
|
|
26
26
|
DEVSERVICES_DIR_NAME = "devservices"
|
|
27
27
|
CONFIG_FILE_NAME = "config.yml"
|
|
28
|
-
PROGRAMS_CONF_FILE_NAME = "programs.conf"
|
|
29
28
|
DOCKER_CONFIG_DIR = os.environ.get("DOCKER_CONFIG", os.path.expanduser("~/.docker"))
|
|
30
29
|
DOCKER_USER_PLUGIN_DIR = os.path.join(DOCKER_CONFIG_DIR, "cli-plugins/")
|
|
31
30
|
|
|
@@ -10,6 +10,7 @@ import xmlrpc.client
|
|
|
10
10
|
from enum import IntEnum
|
|
11
11
|
from typing import TypedDict
|
|
12
12
|
|
|
13
|
+
import yaml
|
|
13
14
|
from sentry_sdk import capture_exception
|
|
14
15
|
from supervisor.options import ServerOptions
|
|
15
16
|
|
|
@@ -91,24 +92,81 @@ class ProcessInfo(TypedDict):
|
|
|
91
92
|
group: str
|
|
92
93
|
|
|
93
94
|
|
|
95
|
+
class SupervisorProgramConfig(TypedDict, total=False):
|
|
96
|
+
"""Supervisor program configuration."""
|
|
97
|
+
|
|
98
|
+
command: str
|
|
99
|
+
autostart: str | bool
|
|
100
|
+
autorestart: str | bool
|
|
101
|
+
directory: str
|
|
102
|
+
environment: str
|
|
103
|
+
user: str
|
|
104
|
+
priority: str | int
|
|
105
|
+
startsecs: str | int
|
|
106
|
+
startretries: str | int
|
|
107
|
+
stdout_logfile: str
|
|
108
|
+
stderr_logfile: str
|
|
109
|
+
redirect_stderr: str | bool
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
ProgramData = dict[str, SupervisorProgramConfig]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# Default values for supervisor program configuration
|
|
116
|
+
SUPERVISOR_PROGRAM_DEFAULTS = {
|
|
117
|
+
"autostart": "false",
|
|
118
|
+
"autorestart": "true",
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
94
122
|
class SupervisorManager:
|
|
95
|
-
def __init__(
|
|
123
|
+
def __init__(
|
|
124
|
+
self,
|
|
125
|
+
service_name: str,
|
|
126
|
+
service_config_path: str,
|
|
127
|
+
) -> None:
|
|
96
128
|
self.service_name = service_name
|
|
97
|
-
if not os.path.exists(config_file_path):
|
|
98
|
-
raise SupervisorConfigError(
|
|
99
|
-
f"Config file {config_file_path} does not exist"
|
|
100
|
-
)
|
|
101
129
|
self.socket_path = os.path.join(
|
|
102
130
|
DEVSERVICES_SUPERVISOR_CONFIG_DIR, f"{service_name}.sock"
|
|
103
131
|
)
|
|
104
|
-
self.config_file_path = self._extend_config_file(config_file_path)
|
|
105
132
|
|
|
106
|
-
|
|
107
|
-
|
|
133
|
+
# Load service config and extract x-programs data
|
|
134
|
+
if os.path.exists(service_config_path):
|
|
135
|
+
with open(service_config_path, "r", encoding="utf-8") as stream:
|
|
136
|
+
config = yaml.safe_load(stream)
|
|
137
|
+
else:
|
|
138
|
+
raise SupervisorConfigError(f"Config file {service_config_path} not found")
|
|
139
|
+
|
|
140
|
+
if config is None:
|
|
141
|
+
raise SupervisorConfigError(f"Config file {service_config_path} is empty")
|
|
108
142
|
|
|
143
|
+
programs_data: ProgramData = config.get("x-programs", {})
|
|
144
|
+
|
|
145
|
+
self.has_programs = len(programs_data.keys()) > 0
|
|
146
|
+
|
|
147
|
+
# Generate supervisor config file from x-programs data
|
|
148
|
+
self.config_file_path = self._generate_config_from_programs_data(programs_data)
|
|
149
|
+
|
|
150
|
+
def _generate_config_from_programs_data(self, programs_data: ProgramData) -> str:
|
|
109
151
|
config = configparser.ConfigParser()
|
|
110
152
|
|
|
111
|
-
|
|
153
|
+
# Add program sections
|
|
154
|
+
for program_name, program_config in programs_data.items():
|
|
155
|
+
section_name = f"program:{program_name}"
|
|
156
|
+
config[section_name] = {}
|
|
157
|
+
|
|
158
|
+
# Apply defaults for any missing configuration values
|
|
159
|
+
program_config_with_defaults = {
|
|
160
|
+
**SUPERVISOR_PROGRAM_DEFAULTS,
|
|
161
|
+
**program_config,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for key, value in program_config_with_defaults.items():
|
|
165
|
+
if isinstance(value, bool):
|
|
166
|
+
config[section_name][key] = str(value).lower()
|
|
167
|
+
else:
|
|
168
|
+
config[section_name][key] = str(value)
|
|
169
|
+
|
|
112
170
|
os.makedirs(DEVSERVICES_SUPERVISOR_CONFIG_DIR, exist_ok=True)
|
|
113
171
|
|
|
114
172
|
# Set unix http server to use the socket path
|
|
@@ -129,13 +187,13 @@ class SupervisorManager:
|
|
|
129
187
|
"supervisor.rpcinterface_factory": "supervisor.rpcinterface:make_main_rpcinterface"
|
|
130
188
|
}
|
|
131
189
|
|
|
132
|
-
|
|
190
|
+
config_file_path = os.path.join(
|
|
133
191
|
DEVSERVICES_SUPERVISOR_CONFIG_DIR, f"{self.service_name}.processes.conf"
|
|
134
192
|
)
|
|
135
|
-
with open(
|
|
193
|
+
with open(config_file_path, "w") as f:
|
|
136
194
|
config.write(f)
|
|
137
195
|
|
|
138
|
-
return
|
|
196
|
+
return config_file_path
|
|
139
197
|
|
|
140
198
|
def _get_rpc_client(self) -> xmlrpc.client.ServerProxy:
|
|
141
199
|
"""Get or create an XML-RPC client that connects to the supervisor daemon."""
|
|
@@ -317,6 +375,9 @@ class SupervisorManager:
|
|
|
317
375
|
def get_all_process_info(self) -> dict[str, ProcessInfo]:
|
|
318
376
|
"""Get status information for all supervisor programs."""
|
|
319
377
|
# Check if supervisor client is up first, return empty list if down
|
|
378
|
+
if not self.has_programs:
|
|
379
|
+
return {}
|
|
380
|
+
|
|
320
381
|
try:
|
|
321
382
|
client = self._get_rpc_client()
|
|
322
383
|
client.supervisor.getState()
|
|
@@ -8,7 +8,6 @@ from pathlib import Path
|
|
|
8
8
|
import yaml
|
|
9
9
|
|
|
10
10
|
from devservices.constants import DEVSERVICES_DIR_NAME
|
|
11
|
-
from devservices.constants import PROGRAMS_CONF_FILE_NAME
|
|
12
11
|
|
|
13
12
|
TESTING_DIR = os.path.abspath(os.path.dirname(__file__))
|
|
14
13
|
|
|
@@ -27,14 +26,6 @@ def create_config_file(
|
|
|
27
26
|
yaml.dump(config, f, sort_keys=False, default_flow_style=False)
|
|
28
27
|
|
|
29
28
|
|
|
30
|
-
def create_programs_conf_file(tmp_path: Path, config: str) -> None:
|
|
31
|
-
devservices_dir = Path(tmp_path, DEVSERVICES_DIR_NAME)
|
|
32
|
-
devservices_dir.mkdir(parents=True, exist_ok=True)
|
|
33
|
-
tmp_file = Path(devservices_dir, PROGRAMS_CONF_FILE_NAME)
|
|
34
|
-
with tmp_file.open("w") as f:
|
|
35
|
-
f.write(config)
|
|
36
|
-
|
|
37
|
-
|
|
38
29
|
def run_git_command(command: list[str], cwd: Path) -> None:
|
|
39
30
|
subprocess.run(
|
|
40
31
|
["git", *command], cwd=cwd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
|
@@ -16,10 +16,8 @@ from devservices.configs.service_config import ServiceConfig
|
|
|
16
16
|
from devservices.constants import CONFIG_FILE_NAME
|
|
17
17
|
from devservices.constants import DependencyType
|
|
18
18
|
from devservices.constants import DEVSERVICES_DIR_NAME
|
|
19
|
-
from devservices.constants import PROGRAMS_CONF_FILE_NAME
|
|
20
19
|
from devservices.exceptions import ConfigError
|
|
21
20
|
from devservices.exceptions import ServiceNotFoundError
|
|
22
|
-
from devservices.exceptions import SupervisorConfigError
|
|
23
21
|
from devservices.exceptions import SupervisorError
|
|
24
22
|
from devservices.utils.dependencies import install_and_verify_dependencies
|
|
25
23
|
from devservices.utils.docker_compose import DockerComposeCommand
|
|
@@ -29,7 +27,6 @@ from devservices.utils.state import State
|
|
|
29
27
|
from devservices.utils.state import StateTables
|
|
30
28
|
from testing.utils import create_config_file
|
|
31
29
|
from testing.utils import create_mock_git_repo
|
|
32
|
-
from testing.utils import create_programs_conf_file
|
|
33
30
|
from testing.utils import run_git_command
|
|
34
31
|
|
|
35
32
|
|
|
@@ -1367,6 +1364,11 @@ def test_down_supervisor_program_error(
|
|
|
1367
1364
|
},
|
|
1368
1365
|
"modes": {"default": ["supervisor-program"]},
|
|
1369
1366
|
},
|
|
1367
|
+
"x-programs": {
|
|
1368
|
+
"supervisor-program": {
|
|
1369
|
+
"command": "echo 'Hello, world!'",
|
|
1370
|
+
}
|
|
1371
|
+
},
|
|
1370
1372
|
"services": {
|
|
1371
1373
|
"redis": {"image": "redis:6.2.14-alpine"},
|
|
1372
1374
|
"clickhouse": {
|
|
@@ -1379,12 +1381,6 @@ def test_down_supervisor_program_error(
|
|
|
1379
1381
|
create_config_file(service_path, config)
|
|
1380
1382
|
os.chdir(service_path)
|
|
1381
1383
|
|
|
1382
|
-
supervisor_program_config = """
|
|
1383
|
-
[program:supervisor-program]
|
|
1384
|
-
command=echo "Hello, world!"
|
|
1385
|
-
"""
|
|
1386
|
-
create_programs_conf_file(service_path, supervisor_program_config)
|
|
1387
|
-
|
|
1388
1384
|
args = Namespace(service_name=None, debug=False, exclude_local=False)
|
|
1389
1385
|
|
|
1390
1386
|
with (
|
|
@@ -1432,6 +1428,11 @@ def test_down_supervisor_program_success(
|
|
|
1432
1428
|
},
|
|
1433
1429
|
"modes": {"default": ["supervisor-program"]},
|
|
1434
1430
|
},
|
|
1431
|
+
"x-programs": {
|
|
1432
|
+
"supervisor-program": {
|
|
1433
|
+
"command": "echo 'Hello, world!'",
|
|
1434
|
+
}
|
|
1435
|
+
},
|
|
1435
1436
|
"services": {
|
|
1436
1437
|
"redis": {"image": "redis:6.2.14-alpine"},
|
|
1437
1438
|
"clickhouse": {
|
|
@@ -1444,12 +1445,6 @@ def test_down_supervisor_program_success(
|
|
|
1444
1445
|
create_config_file(service_path, config)
|
|
1445
1446
|
os.chdir(service_path)
|
|
1446
1447
|
|
|
1447
|
-
supervisor_program_config = """
|
|
1448
|
-
[program:supervisor-program]
|
|
1449
|
-
command=echo "Hello, world!"
|
|
1450
|
-
"""
|
|
1451
|
-
create_programs_conf_file(service_path, supervisor_program_config)
|
|
1452
|
-
|
|
1453
1448
|
args = Namespace(service_name=None, debug=False, exclude_local=False)
|
|
1454
1449
|
|
|
1455
1450
|
with (
|
|
@@ -1478,42 +1473,6 @@ command=echo "Hello, world!"
|
|
|
1478
1473
|
assert "example-service stopped" in captured.out.strip()
|
|
1479
1474
|
|
|
1480
1475
|
|
|
1481
|
-
@mock.patch("devservices.utils.supervisor.SupervisorManager.stop_supervisor_daemon")
|
|
1482
|
-
@mock.patch("devservices.utils.supervisor.SupervisorManager.stop_process")
|
|
1483
|
-
def test_bring_down_supervisor_programs_no_programs_config(
|
|
1484
|
-
mock_stop_process: mock.Mock,
|
|
1485
|
-
mock_stop_supervisor_daemon: mock.Mock,
|
|
1486
|
-
tmp_path: Path,
|
|
1487
|
-
) -> None:
|
|
1488
|
-
service_config = ServiceConfig(
|
|
1489
|
-
version=0.1,
|
|
1490
|
-
service_name="test-service",
|
|
1491
|
-
dependencies={
|
|
1492
|
-
"supervisor-program": Dependency(
|
|
1493
|
-
description="Supervisor program",
|
|
1494
|
-
dependency_type=DependencyType.SUPERVISOR,
|
|
1495
|
-
),
|
|
1496
|
-
},
|
|
1497
|
-
modes={"default": ["supervisor-program"]},
|
|
1498
|
-
)
|
|
1499
|
-
service = Service(
|
|
1500
|
-
name="test-service",
|
|
1501
|
-
repo_path=str(tmp_path),
|
|
1502
|
-
config=service_config,
|
|
1503
|
-
)
|
|
1504
|
-
|
|
1505
|
-
status = mock.MagicMock()
|
|
1506
|
-
|
|
1507
|
-
with pytest.raises(
|
|
1508
|
-
SupervisorConfigError,
|
|
1509
|
-
match=f"Config file {tmp_path / DEVSERVICES_DIR_NAME / PROGRAMS_CONF_FILE_NAME} does not exist",
|
|
1510
|
-
):
|
|
1511
|
-
bring_down_supervisor_programs(["supervisor-program"], service, status)
|
|
1512
|
-
|
|
1513
|
-
mock_stop_supervisor_daemon.assert_not_called()
|
|
1514
|
-
mock_stop_process.assert_not_called()
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
1476
|
@mock.patch("devservices.utils.supervisor.SupervisorManager.stop_supervisor_daemon")
|
|
1518
1477
|
@mock.patch("devservices.utils.supervisor.SupervisorManager.stop_process")
|
|
1519
1478
|
def test_bring_down_supervisor_programs_empty_list(
|
|
@@ -1573,14 +1532,20 @@ def test_bring_down_supervisor_programs_success(
|
|
|
1573
1532
|
config=service_config,
|
|
1574
1533
|
)
|
|
1575
1534
|
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
""
|
|
1535
|
+
create_config_file(
|
|
1536
|
+
tmp_path,
|
|
1537
|
+
{
|
|
1538
|
+
"x-sentry-service-config": {
|
|
1539
|
+
"version": 0.1,
|
|
1540
|
+
"service_name": "test-service",
|
|
1541
|
+
},
|
|
1542
|
+
"x-programs": {
|
|
1543
|
+
"supervisor-program": {
|
|
1544
|
+
"command": "echo 'Hello, world!'",
|
|
1545
|
+
}
|
|
1546
|
+
},
|
|
1547
|
+
"services": {},
|
|
1548
|
+
},
|
|
1584
1549
|
)
|
|
1585
1550
|
|
|
1586
1551
|
status = mock.MagicMock()
|