devservices 1.0.2__tar.gz → 1.0.3__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.0.2 → devservices-1.0.3}/PKG-INFO +1 -1
- {devservices-1.0.2 → devservices-1.0.3}/README.md +4 -4
- {devservices-1.0.2 → devservices-1.0.3}/devservices/commands/check_for_update.py +1 -1
- devservices-1.0.3/devservices/commands/down.py +145 -0
- {devservices-1.0.2 → devservices-1.0.3}/devservices/commands/list_dependencies.py +6 -1
- devservices-1.0.3/devservices/commands/logs.py +120 -0
- {devservices-1.0.2 → devservices-1.0.3}/devservices/commands/purge.py +32 -1
- {devservices-1.0.2 → devservices-1.0.3}/devservices/commands/status.py +62 -10
- devservices-1.0.3/devservices/commands/up.py +133 -0
- {devservices-1.0.2 → devservices-1.0.3}/devservices/commands/update.py +2 -2
- {devservices-1.0.2 → devservices-1.0.3}/devservices/constants.py +2 -0
- {devservices-1.0.2 → devservices-1.0.3}/devservices/exceptions.py +11 -0
- {devservices-1.0.2 → devservices-1.0.3}/devservices/main.py +24 -14
- {devservices-1.0.2 → devservices-1.0.3}/devservices/utils/dependencies.py +18 -7
- {devservices-1.0.2 → devservices-1.0.3}/devservices/utils/docker_compose.py +13 -52
- {devservices-1.0.2 → devservices-1.0.3}/devservices.egg-info/PKG-INFO +1 -1
- {devservices-1.0.2 → devservices-1.0.3}/devservices.egg-info/SOURCES.txt +4 -4
- {devservices-1.0.2 → devservices-1.0.3}/pyproject.toml +1 -1
- devservices-1.0.2/tests/commands/test_stop.py → devservices-1.0.3/tests/commands/test_down.py +98 -7
- devservices-1.0.3/tests/commands/test_logs.py +150 -0
- {devservices-1.0.2 → devservices-1.0.3}/tests/commands/test_purge.py +39 -4
- devservices-1.0.3/tests/commands/test_up.py +338 -0
- {devservices-1.0.2 → devservices-1.0.3}/tests/utils/test_dependencies.py +116 -0
- {devservices-1.0.2 → devservices-1.0.3}/tests/utils/test_docker_compose.py +5 -5
- devservices-1.0.2/devservices/commands/logs.py +0 -68
- devservices-1.0.2/devservices/commands/start.py +0 -70
- devservices-1.0.2/devservices/commands/stop.py +0 -78
- devservices-1.0.2/tests/commands/test_logs.py +0 -107
- devservices-1.0.2/tests/commands/test_start.py +0 -183
- {devservices-1.0.2 → devservices-1.0.3}/LICENSE.md +0 -0
- {devservices-1.0.2 → devservices-1.0.3}/devservices/__init__.py +0 -0
- {devservices-1.0.2 → devservices-1.0.3}/devservices/commands/__init__.py +0 -0
- {devservices-1.0.2 → devservices-1.0.3}/devservices/commands/list_services.py +0 -0
- {devservices-1.0.2 → devservices-1.0.3}/devservices/configs/service_config.py +0 -0
- {devservices-1.0.2 → devservices-1.0.3}/devservices/utils/__init__.py +0 -0
- {devservices-1.0.2 → devservices-1.0.3}/devservices/utils/console.py +0 -0
- {devservices-1.0.2 → devservices-1.0.3}/devservices/utils/devenv.py +0 -0
- {devservices-1.0.2 → devservices-1.0.3}/devservices/utils/docker.py +0 -0
- {devservices-1.0.2 → devservices-1.0.3}/devservices/utils/file_lock.py +0 -0
- {devservices-1.0.2 → devservices-1.0.3}/devservices/utils/install_binary.py +0 -0
- {devservices-1.0.2 → devservices-1.0.3}/devservices/utils/services.py +0 -0
- {devservices-1.0.2 → devservices-1.0.3}/devservices/utils/state.py +0 -0
- {devservices-1.0.2 → devservices-1.0.3}/devservices.egg-info/dependency_links.txt +0 -0
- {devservices-1.0.2 → devservices-1.0.3}/devservices.egg-info/entry_points.txt +0 -0
- {devservices-1.0.2 → devservices-1.0.3}/devservices.egg-info/requires.txt +0 -0
- {devservices-1.0.2 → devservices-1.0.3}/devservices.egg-info/top_level.txt +0 -0
- {devservices-1.0.2 → devservices-1.0.3}/setup.cfg +0 -0
- {devservices-1.0.2 → devservices-1.0.3}/testing/__init__.py +0 -0
- {devservices-1.0.2 → devservices-1.0.3}/testing/utils.py +0 -0
- {devservices-1.0.2 → devservices-1.0.3}/tests/__init__.py +0 -0
- {devservices-1.0.2 → devservices-1.0.3}/tests/commands/test_list_services.py +0 -0
- {devservices-1.0.2 → devservices-1.0.3}/tests/commands/test_update.py +0 -0
- {devservices-1.0.2 → devservices-1.0.3}/tests/configs/test_service_config.py +0 -0
- {devservices-1.0.2 → devservices-1.0.3}/tests/conftest.py +0 -0
- {devservices-1.0.2 → devservices-1.0.3}/tests/utils/test_docker.py +0 -0
- {devservices-1.0.2 → devservices-1.0.3}/tests/utils/test_install_binary.py +0 -0
- {devservices-1.0.2 → devservices-1.0.3}/tests/utils/test_state.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# devservices
|
|
2
2
|
|
|
3
|
-
A standalone cli tool used to manage dependencies for services. It simplifies the process of
|
|
3
|
+
A standalone cli tool used to manage dependencies for services. It simplifies the process of managing services for development purposes and bringing services up/down.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
@@ -11,7 +11,7 @@ A standalone cli tool used to manage dependencies for services. It simplifies th
|
|
|
11
11
|
The recommended way to install devservices is through a virtualenv in the requirements.txt.
|
|
12
12
|
|
|
13
13
|
```
|
|
14
|
-
devservices==1.0.
|
|
14
|
+
devservices==1.0.3
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
|
|
@@ -23,8 +23,8 @@ devservices provides several commands to manage your services:
|
|
|
23
23
|
|
|
24
24
|
NOTE: service-name is an optional parameter. If not provided, devservices will attempt to automatically find a devservices configuration in the current directory in order to proceed.
|
|
25
25
|
|
|
26
|
-
- `devservices
|
|
27
|
-
- `devservices
|
|
26
|
+
- `devservices up <service-name>`: Bring up a service and its dependencies.
|
|
27
|
+
- `devservices down <service-name>`: Bring down service including its dependencies.
|
|
28
28
|
- `devservices status <service-name>`: Display the current status of all services, including their dependencies and ports.
|
|
29
29
|
- `devservices logs <service-name>`: View logs for a specific service.
|
|
30
30
|
- `devservices list-services`: List all available Sentry services.
|
|
@@ -4,7 +4,7 @@ import json
|
|
|
4
4
|
from urllib.request import urlopen
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
def check_for_update(
|
|
7
|
+
def check_for_update() -> str | None:
|
|
8
8
|
url = "https://api.github.com/repos/getsentry/devservices/releases/latest"
|
|
9
9
|
with urlopen(url) as response:
|
|
10
10
|
if response.status == 200:
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import concurrent.futures
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
from argparse import _SubParsersAction
|
|
7
|
+
from argparse import ArgumentParser
|
|
8
|
+
from argparse import Namespace
|
|
9
|
+
|
|
10
|
+
from sentry_sdk import capture_exception
|
|
11
|
+
|
|
12
|
+
from devservices.constants import CONFIG_FILE_NAME
|
|
13
|
+
from devservices.constants import DEPENDENCY_CONFIG_VERSION
|
|
14
|
+
from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR
|
|
15
|
+
from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
|
|
16
|
+
from devservices.constants import DEVSERVICES_DIR_NAME
|
|
17
|
+
from devservices.constants import DOCKER_COMPOSE_COMMAND_LENGTH
|
|
18
|
+
from devservices.exceptions import ConfigError
|
|
19
|
+
from devservices.exceptions import DependencyError
|
|
20
|
+
from devservices.exceptions import DockerComposeError
|
|
21
|
+
from devservices.exceptions import ServiceNotFoundError
|
|
22
|
+
from devservices.utils.console import Console
|
|
23
|
+
from devservices.utils.console import Status
|
|
24
|
+
from devservices.utils.dependencies import get_non_shared_remote_dependencies
|
|
25
|
+
from devservices.utils.dependencies import install_and_verify_dependencies
|
|
26
|
+
from devservices.utils.dependencies import InstalledRemoteDependency
|
|
27
|
+
from devservices.utils.docker_compose import get_docker_compose_commands_to_run
|
|
28
|
+
from devservices.utils.docker_compose import run_cmd
|
|
29
|
+
from devservices.utils.services import find_matching_service
|
|
30
|
+
from devservices.utils.services import Service
|
|
31
|
+
from devservices.utils.state import State
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
35
|
+
parser = subparsers.add_parser(
|
|
36
|
+
"down", help="Bring down a service and its dependencies"
|
|
37
|
+
)
|
|
38
|
+
parser.add_argument(
|
|
39
|
+
"service_name",
|
|
40
|
+
help="Name of the service to bring down",
|
|
41
|
+
nargs="?",
|
|
42
|
+
default=None,
|
|
43
|
+
)
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
"--debug",
|
|
46
|
+
help="Enable debug mode",
|
|
47
|
+
action="store_true",
|
|
48
|
+
default=False,
|
|
49
|
+
)
|
|
50
|
+
parser.set_defaults(func=down)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def down(args: Namespace) -> None:
|
|
54
|
+
"""Bring down a service and its dependencies."""
|
|
55
|
+
console = Console()
|
|
56
|
+
service_name = args.service_name
|
|
57
|
+
try:
|
|
58
|
+
service = find_matching_service(service_name)
|
|
59
|
+
except (ConfigError, ServiceNotFoundError) as e:
|
|
60
|
+
capture_exception(e)
|
|
61
|
+
console.failure(str(e))
|
|
62
|
+
exit(1)
|
|
63
|
+
|
|
64
|
+
modes = service.config.modes
|
|
65
|
+
|
|
66
|
+
state = State()
|
|
67
|
+
started_services = state.get_started_services()
|
|
68
|
+
if service.name not in started_services:
|
|
69
|
+
console.warning(f"{service.name} is not running")
|
|
70
|
+
exit(0)
|
|
71
|
+
|
|
72
|
+
mode = state.get_mode_for_service(service.name) or "default"
|
|
73
|
+
mode_dependencies = modes[mode]
|
|
74
|
+
|
|
75
|
+
with Status(
|
|
76
|
+
lambda: console.warning(f"Stopping {service.name}"),
|
|
77
|
+
lambda: console.success(f"{service.name} stopped"),
|
|
78
|
+
) as status:
|
|
79
|
+
try:
|
|
80
|
+
remote_dependencies = install_and_verify_dependencies(service, mode=mode)
|
|
81
|
+
except DependencyError as de:
|
|
82
|
+
capture_exception(de)
|
|
83
|
+
status.failure(str(de))
|
|
84
|
+
exit(1)
|
|
85
|
+
remote_dependencies = get_non_shared_remote_dependencies(
|
|
86
|
+
service, remote_dependencies
|
|
87
|
+
)
|
|
88
|
+
try:
|
|
89
|
+
_down(service, remote_dependencies, mode_dependencies, status)
|
|
90
|
+
except DockerComposeError as dce:
|
|
91
|
+
capture_exception(dce)
|
|
92
|
+
status.failure(f"Failed to stop {service.name}: {dce.stderr}")
|
|
93
|
+
exit(1)
|
|
94
|
+
|
|
95
|
+
# TODO: We should factor in healthchecks here before marking service as not running
|
|
96
|
+
state = State()
|
|
97
|
+
state.remove_started_service(service.name)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _bring_down_dependency(
|
|
101
|
+
cmd: list[str], current_env: dict[str, str], status: Status
|
|
102
|
+
) -> subprocess.CompletedProcess[str]:
|
|
103
|
+
# TODO: Get rid of these constants, we need a smarter way to determine the containers being brought down
|
|
104
|
+
for dependency in cmd[DOCKER_COMPOSE_COMMAND_LENGTH:]:
|
|
105
|
+
status.info(f"Stopping {dependency}")
|
|
106
|
+
return run_cmd(cmd, current_env)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _down(
|
|
110
|
+
service: Service,
|
|
111
|
+
remote_dependencies: set[InstalledRemoteDependency],
|
|
112
|
+
mode_dependencies: list[str],
|
|
113
|
+
status: Status,
|
|
114
|
+
) -> None:
|
|
115
|
+
relative_local_dependency_directory = os.path.relpath(
|
|
116
|
+
os.path.join(DEVSERVICES_DEPENDENCIES_CACHE_DIR, DEPENDENCY_CONFIG_VERSION),
|
|
117
|
+
service.repo_path,
|
|
118
|
+
)
|
|
119
|
+
service_config_file_path = os.path.join(
|
|
120
|
+
service.repo_path, DEVSERVICES_DIR_NAME, CONFIG_FILE_NAME
|
|
121
|
+
)
|
|
122
|
+
# Set the environment variable for the local dependencies directory to be used by docker compose
|
|
123
|
+
current_env = os.environ.copy()
|
|
124
|
+
current_env[
|
|
125
|
+
DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
|
|
126
|
+
] = relative_local_dependency_directory
|
|
127
|
+
docker_compose_commands = get_docker_compose_commands_to_run(
|
|
128
|
+
service=service,
|
|
129
|
+
remote_dependencies=remote_dependencies,
|
|
130
|
+
current_env=current_env,
|
|
131
|
+
command="down",
|
|
132
|
+
options=[],
|
|
133
|
+
service_config_file_path=service_config_file_path,
|
|
134
|
+
mode_dependencies=mode_dependencies,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
cmd_outputs = []
|
|
138
|
+
|
|
139
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
140
|
+
futures = [
|
|
141
|
+
executor.submit(_bring_down_dependency, cmd, current_env, status)
|
|
142
|
+
for cmd in docker_compose_commands
|
|
143
|
+
]
|
|
144
|
+
for future in concurrent.futures.as_completed(futures):
|
|
145
|
+
cmd_outputs.append(future.result())
|
|
@@ -4,6 +4,10 @@ from argparse import _SubParsersAction
|
|
|
4
4
|
from argparse import ArgumentParser
|
|
5
5
|
from argparse import Namespace
|
|
6
6
|
|
|
7
|
+
from sentry_sdk import capture_exception
|
|
8
|
+
|
|
9
|
+
from devservices.exceptions import ConfigError
|
|
10
|
+
from devservices.exceptions import ServiceNotFoundError
|
|
7
11
|
from devservices.utils.console import Console
|
|
8
12
|
from devservices.utils.services import find_matching_service
|
|
9
13
|
|
|
@@ -28,7 +32,8 @@ def list_dependencies(args: Namespace) -> None:
|
|
|
28
32
|
|
|
29
33
|
try:
|
|
30
34
|
service = find_matching_service(service_name)
|
|
31
|
-
except
|
|
35
|
+
except (ConfigError, ServiceNotFoundError) as e:
|
|
36
|
+
capture_exception(e)
|
|
32
37
|
console.failure(str(e))
|
|
33
38
|
exit(1)
|
|
34
39
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import concurrent.futures
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
from argparse import _SubParsersAction
|
|
7
|
+
from argparse import ArgumentParser
|
|
8
|
+
from argparse import Namespace
|
|
9
|
+
|
|
10
|
+
from sentry_sdk import capture_exception
|
|
11
|
+
|
|
12
|
+
from devservices.constants import CONFIG_FILE_NAME
|
|
13
|
+
from devservices.constants import DEPENDENCY_CONFIG_VERSION
|
|
14
|
+
from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR
|
|
15
|
+
from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
|
|
16
|
+
from devservices.constants import DEVSERVICES_DIR_NAME
|
|
17
|
+
from devservices.constants import MAX_LOG_LINES
|
|
18
|
+
from devservices.exceptions import ConfigError
|
|
19
|
+
from devservices.exceptions import DependencyError
|
|
20
|
+
from devservices.exceptions import DockerComposeError
|
|
21
|
+
from devservices.exceptions import ServiceNotFoundError
|
|
22
|
+
from devservices.utils.console import Console
|
|
23
|
+
from devservices.utils.dependencies import install_and_verify_dependencies
|
|
24
|
+
from devservices.utils.dependencies import InstalledRemoteDependency
|
|
25
|
+
from devservices.utils.docker_compose import get_docker_compose_commands_to_run
|
|
26
|
+
from devservices.utils.docker_compose import run_cmd
|
|
27
|
+
from devservices.utils.services import find_matching_service
|
|
28
|
+
from devservices.utils.services import Service
|
|
29
|
+
from devservices.utils.state import State
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
33
|
+
parser = subparsers.add_parser("logs", help="View logs for a service")
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
"service_name",
|
|
36
|
+
help="Name of the service to view logs for",
|
|
37
|
+
nargs="?",
|
|
38
|
+
default=None,
|
|
39
|
+
)
|
|
40
|
+
parser.set_defaults(func=logs)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def logs(args: Namespace) -> None:
|
|
44
|
+
"""View the logs for a specified service."""
|
|
45
|
+
console = Console()
|
|
46
|
+
service_name = args.service_name
|
|
47
|
+
try:
|
|
48
|
+
service = find_matching_service(service_name)
|
|
49
|
+
except (ConfigError, ServiceNotFoundError) as e:
|
|
50
|
+
capture_exception(e)
|
|
51
|
+
console.failure(str(e))
|
|
52
|
+
exit(1)
|
|
53
|
+
|
|
54
|
+
modes = service.config.modes
|
|
55
|
+
# TODO: allow custom modes to be used
|
|
56
|
+
mode_to_use = "default"
|
|
57
|
+
mode_dependencies = modes[mode_to_use]
|
|
58
|
+
|
|
59
|
+
state = State()
|
|
60
|
+
running_services = state.get_started_services()
|
|
61
|
+
if service.name not in running_services:
|
|
62
|
+
console.warning(f"Service {service.name} is not running")
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
remote_dependencies = install_and_verify_dependencies(service)
|
|
67
|
+
except DependencyError as de:
|
|
68
|
+
capture_exception(de)
|
|
69
|
+
console.failure(str(de))
|
|
70
|
+
exit(1)
|
|
71
|
+
try:
|
|
72
|
+
logs_output = _logs(service, remote_dependencies, mode_dependencies)
|
|
73
|
+
except DockerComposeError as dce:
|
|
74
|
+
capture_exception(dce)
|
|
75
|
+
console.failure(f"Failed to get logs for {service.name}: {dce.stderr}")
|
|
76
|
+
exit(1)
|
|
77
|
+
for log in logs_output:
|
|
78
|
+
log_stdout: str | None = log.stdout
|
|
79
|
+
if log_stdout is not None:
|
|
80
|
+
console.info(log_stdout)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _logs(
|
|
84
|
+
service: Service,
|
|
85
|
+
remote_dependencies: set[InstalledRemoteDependency],
|
|
86
|
+
mode_dependencies: list[str],
|
|
87
|
+
) -> list[subprocess.CompletedProcess[str]]:
|
|
88
|
+
relative_local_dependency_directory = os.path.relpath(
|
|
89
|
+
os.path.join(DEVSERVICES_DEPENDENCIES_CACHE_DIR, DEPENDENCY_CONFIG_VERSION),
|
|
90
|
+
service.repo_path,
|
|
91
|
+
)
|
|
92
|
+
service_config_file_path = os.path.join(
|
|
93
|
+
service.repo_path, DEVSERVICES_DIR_NAME, CONFIG_FILE_NAME
|
|
94
|
+
)
|
|
95
|
+
# Set the environment variable for the local dependencies directory to be used by docker compose
|
|
96
|
+
current_env = os.environ.copy()
|
|
97
|
+
current_env[
|
|
98
|
+
DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
|
|
99
|
+
] = relative_local_dependency_directory
|
|
100
|
+
docker_compose_commands = get_docker_compose_commands_to_run(
|
|
101
|
+
service=service,
|
|
102
|
+
remote_dependencies=remote_dependencies,
|
|
103
|
+
current_env=current_env,
|
|
104
|
+
command="logs",
|
|
105
|
+
options=["-n", MAX_LOG_LINES],
|
|
106
|
+
service_config_file_path=service_config_file_path,
|
|
107
|
+
mode_dependencies=mode_dependencies,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
cmd_outputs = []
|
|
111
|
+
|
|
112
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
113
|
+
futures = [
|
|
114
|
+
executor.submit(run_cmd, cmd, current_env)
|
|
115
|
+
for cmd in docker_compose_commands
|
|
116
|
+
]
|
|
117
|
+
for future in concurrent.futures.as_completed(futures):
|
|
118
|
+
cmd_outputs.append(future.result())
|
|
119
|
+
|
|
120
|
+
return cmd_outputs
|
|
@@ -2,11 +2,13 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
import shutil
|
|
5
|
+
import subprocess
|
|
5
6
|
from argparse import _SubParsersAction
|
|
6
7
|
from argparse import ArgumentParser
|
|
7
8
|
from argparse import Namespace
|
|
8
9
|
|
|
9
10
|
from devservices.constants import DEVSERVICES_CACHE_DIR
|
|
11
|
+
from devservices.constants import DOCKER_NETWORK_NAME
|
|
10
12
|
from devservices.exceptions import DockerDaemonNotRunningError
|
|
11
13
|
from devservices.utils.console import Console
|
|
12
14
|
from devservices.utils.console import Status
|
|
@@ -19,9 +21,10 @@ def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
|
19
21
|
parser.set_defaults(func=purge)
|
|
20
22
|
|
|
21
23
|
|
|
22
|
-
def purge(
|
|
24
|
+
def purge(_args: Namespace) -> None:
|
|
23
25
|
"""Purge the local devservices cache."""
|
|
24
26
|
console = Console()
|
|
27
|
+
|
|
25
28
|
# Prompt the user to stop all running containers
|
|
26
29
|
should_stop_containers = console.confirm(
|
|
27
30
|
"Warning: Purging stops all running containers and clears devservices state. Would you like to continue?"
|
|
@@ -47,4 +50,32 @@ def purge(args: Namespace) -> None:
|
|
|
47
50
|
except DockerDaemonNotRunningError:
|
|
48
51
|
console.warning("The docker daemon not running, no containers to stop")
|
|
49
52
|
|
|
53
|
+
console.warning("Removing any devservices networks")
|
|
54
|
+
devservices_networks = (
|
|
55
|
+
subprocess.check_output(
|
|
56
|
+
[
|
|
57
|
+
"docker",
|
|
58
|
+
"network",
|
|
59
|
+
"ls",
|
|
60
|
+
"--filter",
|
|
61
|
+
f"name={DOCKER_NETWORK_NAME}",
|
|
62
|
+
"--format",
|
|
63
|
+
"{{.ID}}",
|
|
64
|
+
]
|
|
65
|
+
)
|
|
66
|
+
.decode()
|
|
67
|
+
.strip()
|
|
68
|
+
.splitlines()
|
|
69
|
+
)
|
|
70
|
+
if len(devservices_networks) == 0:
|
|
71
|
+
console.success("No devservices networks found to remove")
|
|
72
|
+
for network in devservices_networks:
|
|
73
|
+
subprocess.run(
|
|
74
|
+
["docker", "network", "rm", network],
|
|
75
|
+
check=True,
|
|
76
|
+
stdout=subprocess.DEVNULL,
|
|
77
|
+
stderr=subprocess.DEVNULL,
|
|
78
|
+
)
|
|
79
|
+
console.success(f"Network {network} removed")
|
|
80
|
+
|
|
50
81
|
console.success("The local devservices cache and state has been purged")
|
|
@@ -1,16 +1,31 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import concurrent.futures
|
|
3
4
|
import json
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
4
7
|
from argparse import _SubParsersAction
|
|
5
8
|
from argparse import ArgumentParser
|
|
6
9
|
from argparse import Namespace
|
|
7
10
|
|
|
11
|
+
from sentry_sdk import capture_exception
|
|
12
|
+
|
|
13
|
+
from devservices.constants import CONFIG_FILE_NAME
|
|
14
|
+
from devservices.constants import DEPENDENCY_CONFIG_VERSION
|
|
15
|
+
from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR
|
|
16
|
+
from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
|
|
17
|
+
from devservices.constants import DEVSERVICES_DIR_NAME
|
|
18
|
+
from devservices.exceptions import ConfigError
|
|
8
19
|
from devservices.exceptions import DependencyError
|
|
9
20
|
from devservices.exceptions import DockerComposeError
|
|
21
|
+
from devservices.exceptions import ServiceNotFoundError
|
|
10
22
|
from devservices.utils.console import Console
|
|
11
23
|
from devservices.utils.dependencies import install_and_verify_dependencies
|
|
12
|
-
from devservices.utils.
|
|
24
|
+
from devservices.utils.dependencies import InstalledRemoteDependency
|
|
25
|
+
from devservices.utils.docker_compose import get_docker_compose_commands_to_run
|
|
26
|
+
from devservices.utils.docker_compose import run_cmd
|
|
13
27
|
from devservices.utils.services import find_matching_service
|
|
28
|
+
from devservices.utils.services import Service
|
|
14
29
|
|
|
15
30
|
LINE_LENGTH = 40
|
|
16
31
|
|
|
@@ -61,12 +76,13 @@ def format_status_output(status_json: str) -> str:
|
|
|
61
76
|
|
|
62
77
|
|
|
63
78
|
def status(args: Namespace) -> None:
|
|
64
|
-
"""
|
|
79
|
+
"""Get the status of a specified service."""
|
|
65
80
|
console = Console()
|
|
66
81
|
service_name = args.service_name
|
|
67
82
|
try:
|
|
68
83
|
service = find_matching_service(service_name)
|
|
69
|
-
except
|
|
84
|
+
except (ConfigError, ServiceNotFoundError) as e:
|
|
85
|
+
capture_exception(e)
|
|
70
86
|
console.failure(str(e))
|
|
71
87
|
exit(1)
|
|
72
88
|
|
|
@@ -78,17 +94,13 @@ def status(args: Namespace) -> None:
|
|
|
78
94
|
try:
|
|
79
95
|
remote_dependencies = install_and_verify_dependencies(service)
|
|
80
96
|
except DependencyError as de:
|
|
97
|
+
capture_exception(de)
|
|
81
98
|
console.failure(str(de))
|
|
82
99
|
exit(1)
|
|
83
100
|
try:
|
|
84
|
-
status_json_results =
|
|
85
|
-
service,
|
|
86
|
-
"ps",
|
|
87
|
-
mode_dependencies,
|
|
88
|
-
remote_dependencies,
|
|
89
|
-
options=["--format", "json"],
|
|
90
|
-
)
|
|
101
|
+
status_json_results = _status(service, remote_dependencies, mode_dependencies)
|
|
91
102
|
except DockerComposeError as dce:
|
|
103
|
+
capture_exception(dce)
|
|
92
104
|
console.failure(f"Failed to get status for {service.name}: {dce.stderr}")
|
|
93
105
|
exit(1)
|
|
94
106
|
|
|
@@ -104,3 +116,43 @@ def status(args: Namespace) -> None:
|
|
|
104
116
|
output += format_status_output(status_json.stdout)
|
|
105
117
|
output += "=" * LINE_LENGTH
|
|
106
118
|
console.info(output + "\n")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _status(
|
|
122
|
+
service: Service,
|
|
123
|
+
remote_dependencies: set[InstalledRemoteDependency],
|
|
124
|
+
mode_dependencies: list[str],
|
|
125
|
+
) -> list[subprocess.CompletedProcess[str]]:
|
|
126
|
+
relative_local_dependency_directory = os.path.relpath(
|
|
127
|
+
os.path.join(DEVSERVICES_DEPENDENCIES_CACHE_DIR, DEPENDENCY_CONFIG_VERSION),
|
|
128
|
+
service.repo_path,
|
|
129
|
+
)
|
|
130
|
+
service_config_file_path = os.path.join(
|
|
131
|
+
service.repo_path, DEVSERVICES_DIR_NAME, CONFIG_FILE_NAME
|
|
132
|
+
)
|
|
133
|
+
# Set the environment variable for the local dependencies directory to be used by docker compose
|
|
134
|
+
current_env = os.environ.copy()
|
|
135
|
+
current_env[
|
|
136
|
+
DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
|
|
137
|
+
] = relative_local_dependency_directory
|
|
138
|
+
docker_compose_commands = get_docker_compose_commands_to_run(
|
|
139
|
+
service=service,
|
|
140
|
+
remote_dependencies=remote_dependencies,
|
|
141
|
+
current_env=current_env,
|
|
142
|
+
command="ps",
|
|
143
|
+
options=["--format", "json"],
|
|
144
|
+
service_config_file_path=service_config_file_path,
|
|
145
|
+
mode_dependencies=mode_dependencies,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
cmd_outputs = []
|
|
149
|
+
|
|
150
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
151
|
+
futures = [
|
|
152
|
+
executor.submit(run_cmd, cmd, current_env)
|
|
153
|
+
for cmd in docker_compose_commands
|
|
154
|
+
]
|
|
155
|
+
for future in concurrent.futures.as_completed(futures):
|
|
156
|
+
cmd_outputs.append(future.result())
|
|
157
|
+
|
|
158
|
+
return cmd_outputs
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
from argparse import _SubParsersAction
|
|
6
|
+
from argparse import ArgumentParser
|
|
7
|
+
from argparse import Namespace
|
|
8
|
+
|
|
9
|
+
from sentry_sdk import capture_exception
|
|
10
|
+
|
|
11
|
+
from devservices.constants import CONFIG_FILE_NAME
|
|
12
|
+
from devservices.constants import DEPENDENCY_CONFIG_VERSION
|
|
13
|
+
from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR
|
|
14
|
+
from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
|
|
15
|
+
from devservices.constants import DEVSERVICES_DIR_NAME
|
|
16
|
+
from devservices.constants import DOCKER_COMPOSE_COMMAND_LENGTH
|
|
17
|
+
from devservices.exceptions import ConfigError
|
|
18
|
+
from devservices.exceptions import DependencyError
|
|
19
|
+
from devservices.exceptions import DockerComposeError
|
|
20
|
+
from devservices.exceptions import ModeDoesNotExistError
|
|
21
|
+
from devservices.exceptions import ServiceNotFoundError
|
|
22
|
+
from devservices.utils.console import Console
|
|
23
|
+
from devservices.utils.console import Status
|
|
24
|
+
from devservices.utils.dependencies import install_and_verify_dependencies
|
|
25
|
+
from devservices.utils.dependencies import InstalledRemoteDependency
|
|
26
|
+
from devservices.utils.docker_compose import get_docker_compose_commands_to_run
|
|
27
|
+
from devservices.utils.docker_compose import run_cmd
|
|
28
|
+
from devservices.utils.services import find_matching_service
|
|
29
|
+
from devservices.utils.services import Service
|
|
30
|
+
from devservices.utils.state import State
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
34
|
+
parser = subparsers.add_parser("up", help="Bring up a service and its dependencies")
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"service_name", help="Name of the service to bring up", nargs="?", default=None
|
|
37
|
+
)
|
|
38
|
+
parser.add_argument(
|
|
39
|
+
"--debug",
|
|
40
|
+
help="Enable debug mode",
|
|
41
|
+
action="store_true",
|
|
42
|
+
default=False,
|
|
43
|
+
)
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
"--mode",
|
|
46
|
+
help="Mode to use for the service",
|
|
47
|
+
default="default",
|
|
48
|
+
)
|
|
49
|
+
parser.set_defaults(func=up)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def up(args: Namespace) -> None:
|
|
53
|
+
"""Bring up a service and its dependencies."""
|
|
54
|
+
console = Console()
|
|
55
|
+
service_name = args.service_name
|
|
56
|
+
try:
|
|
57
|
+
service = find_matching_service(service_name)
|
|
58
|
+
except (ConfigError, ServiceNotFoundError) as e:
|
|
59
|
+
capture_exception(e)
|
|
60
|
+
console.failure(str(e))
|
|
61
|
+
exit(1)
|
|
62
|
+
|
|
63
|
+
modes = service.config.modes
|
|
64
|
+
mode = args.mode
|
|
65
|
+
|
|
66
|
+
with Status(
|
|
67
|
+
lambda: console.warning(f"Starting {service.name}"),
|
|
68
|
+
lambda: console.success(f"{service.name} started"),
|
|
69
|
+
) as status:
|
|
70
|
+
try:
|
|
71
|
+
status.info("Retrieving dependencies")
|
|
72
|
+
remote_dependencies = install_and_verify_dependencies(
|
|
73
|
+
service, force_update_dependencies=True, mode=mode
|
|
74
|
+
)
|
|
75
|
+
except DependencyError as de:
|
|
76
|
+
capture_exception(de)
|
|
77
|
+
status.failure(str(de))
|
|
78
|
+
exit(1)
|
|
79
|
+
except ModeDoesNotExistError as mde:
|
|
80
|
+
status.failure(str(mde))
|
|
81
|
+
exit(1)
|
|
82
|
+
try:
|
|
83
|
+
mode_dependencies = modes[mode]
|
|
84
|
+
_up(service, remote_dependencies, mode_dependencies, status)
|
|
85
|
+
except DockerComposeError as dce:
|
|
86
|
+
capture_exception(dce)
|
|
87
|
+
status.failure(f"Failed to start {service.name}: {dce.stderr}")
|
|
88
|
+
exit(1)
|
|
89
|
+
# TODO: We should factor in healthchecks here before marking service as running
|
|
90
|
+
state = State()
|
|
91
|
+
state.add_started_service(service.name, mode)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _bring_up_dependency(
|
|
95
|
+
cmd: list[str], current_env: dict[str, str], status: Status, len_options: int
|
|
96
|
+
) -> subprocess.CompletedProcess[str]:
|
|
97
|
+
# TODO: Get rid of these constants, we need a smarter way to determine the containers being brought up
|
|
98
|
+
for dependency in cmd[DOCKER_COMPOSE_COMMAND_LENGTH:-len_options]:
|
|
99
|
+
status.info(f"Starting {dependency}")
|
|
100
|
+
return run_cmd(cmd, current_env)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _up(
|
|
104
|
+
service: Service,
|
|
105
|
+
remote_dependencies: set[InstalledRemoteDependency],
|
|
106
|
+
mode_dependencies: list[str],
|
|
107
|
+
status: Status,
|
|
108
|
+
) -> None:
|
|
109
|
+
relative_local_dependency_directory = os.path.relpath(
|
|
110
|
+
os.path.join(DEVSERVICES_DEPENDENCIES_CACHE_DIR, DEPENDENCY_CONFIG_VERSION),
|
|
111
|
+
service.repo_path,
|
|
112
|
+
)
|
|
113
|
+
service_config_file_path = os.path.join(
|
|
114
|
+
service.repo_path, DEVSERVICES_DIR_NAME, CONFIG_FILE_NAME
|
|
115
|
+
)
|
|
116
|
+
# Set the environment variable for the local dependencies directory to be used by docker compose
|
|
117
|
+
current_env = os.environ.copy()
|
|
118
|
+
current_env[
|
|
119
|
+
DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
|
|
120
|
+
] = relative_local_dependency_directory
|
|
121
|
+
options = ["-d"]
|
|
122
|
+
docker_compose_commands = get_docker_compose_commands_to_run(
|
|
123
|
+
service=service,
|
|
124
|
+
remote_dependencies=remote_dependencies,
|
|
125
|
+
current_env=current_env,
|
|
126
|
+
command="up",
|
|
127
|
+
options=options,
|
|
128
|
+
service_config_file_path=service_config_file_path,
|
|
129
|
+
mode_dependencies=mode_dependencies,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
for cmd in docker_compose_commands:
|
|
133
|
+
_bring_up_dependency(cmd, current_env, status, len(options))
|
|
@@ -41,10 +41,10 @@ def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
|
41
41
|
parser.set_defaults(func=update)
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
def update(
|
|
44
|
+
def update(_args: Namespace) -> None:
|
|
45
45
|
console = Console()
|
|
46
46
|
current_version = metadata.version("devservices")
|
|
47
|
-
latest_version = check_for_update(
|
|
47
|
+
latest_version = check_for_update()
|
|
48
48
|
|
|
49
49
|
if latest_version is None:
|
|
50
50
|
raise DevservicesUpdateError("Failed to check for updates.")
|
|
@@ -12,6 +12,7 @@ DEVSERVICES_LOCAL_DIR = os.path.expanduser("~/.local/share/sentry-devservices")
|
|
|
12
12
|
DEVSERVICES_DEPENDENCIES_CACHE_DIR = os.path.join(DEVSERVICES_CACHE_DIR, "dependencies")
|
|
13
13
|
DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY = "DEVSERVICES_DEPENDENCIES_CACHE_DIR"
|
|
14
14
|
STATE_DB_FILE = os.path.join(DEVSERVICES_LOCAL_DIR, "state")
|
|
15
|
+
DOCKER_COMPOSE_COMMAND_LENGTH = 7
|
|
15
16
|
|
|
16
17
|
DEPENDENCY_CONFIG_VERSION = "v1"
|
|
17
18
|
DEPENDENCY_GIT_PARTIAL_CLONE_CONFIG_OPTIONS = {
|
|
@@ -25,3 +26,4 @@ DEVSERVICES_DOWNLOAD_URL = "https://github.com/getsentry/devservices/releases/do
|
|
|
25
26
|
BINARY_PERMISSIONS = 0o755
|
|
26
27
|
MAX_LOG_LINES = "100"
|
|
27
28
|
LOGGER_NAME = "devservices"
|
|
29
|
+
DOCKER_NETWORK_NAME = "devservices"
|
|
@@ -67,6 +67,17 @@ class DockerComposeError(Exception):
|
|
|
67
67
|
self.stderr = stderr
|
|
68
68
|
|
|
69
69
|
|
|
70
|
+
class ModeDoesNotExistError(Exception):
|
|
71
|
+
"""Raised when a mode does not exist."""
|
|
72
|
+
|
|
73
|
+
def __init__(self, service_name: str, mode: str):
|
|
74
|
+
self.service_name = service_name
|
|
75
|
+
self.mode = mode
|
|
76
|
+
|
|
77
|
+
def __str__(self) -> str:
|
|
78
|
+
return f"ModeDoesNotExistError: Mode '{self.mode}' does not exist for service '{self.service_name}'"
|
|
79
|
+
|
|
80
|
+
|
|
70
81
|
class DependencyError(Exception):
|
|
71
82
|
"""Base class for dependency-related errors."""
|
|
72
83
|
|