devservices 1.0.18__tar.gz → 1.1.0__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.18 → devservices-1.1.0}/PKG-INFO +1 -1
- {devservices-1.0.18 → devservices-1.1.0}/README.md +1 -1
- {devservices-1.0.18 → devservices-1.1.0}/devservices/commands/down.py +19 -12
- devservices-1.1.0/devservices/commands/status.py +367 -0
- devservices-1.1.0/devservices/commands/toggle.py +267 -0
- {devservices-1.0.18 → devservices-1.1.0}/devservices/commands/up.py +33 -5
- {devservices-1.0.18 → devservices-1.1.0}/devservices/constants.py +13 -0
- {devservices-1.0.18 → devservices-1.1.0}/devservices/exceptions.py +34 -0
- {devservices-1.0.18 → devservices-1.1.0}/devservices/main.py +2 -0
- {devservices-1.0.18 → devservices-1.1.0}/devservices/utils/console.py +2 -11
- {devservices-1.0.18 → devservices-1.1.0}/devservices/utils/dependencies.py +54 -28
- {devservices-1.0.18 → devservices-1.1.0}/devservices/utils/docker_compose.py +4 -1
- {devservices-1.0.18 → devservices-1.1.0}/devservices/utils/state.py +3 -3
- devservices-1.1.0/devservices/utils/supervisor.py +134 -0
- {devservices-1.0.18 → devservices-1.1.0}/devservices.egg-info/PKG-INFO +1 -1
- {devservices-1.0.18 → devservices-1.1.0}/devservices.egg-info/SOURCES.txt +5 -1
- {devservices-1.0.18 → devservices-1.1.0}/pyproject.toml +1 -1
- devservices-1.1.0/tests/commands/test_status.py +842 -0
- devservices-1.1.0/tests/commands/test_toggle.py +1496 -0
- {devservices-1.0.18 → devservices-1.1.0}/tests/commands/test_up.py +823 -184
- {devservices-1.0.18 → devservices-1.1.0}/tests/utils/test_dependencies.py +369 -30
- {devservices-1.0.18 → devservices-1.1.0}/tests/utils/test_docker_compose.py +6 -2
- devservices-1.1.0/tests/utils/test_supervisor.py +189 -0
- devservices-1.0.18/devservices/commands/status.py +0 -180
- devservices-1.0.18/tests/commands/test_status.py +0 -266
- {devservices-1.0.18 → devservices-1.1.0}/LICENSE.md +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/devservices/__init__.py +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/devservices/commands/__init__.py +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/devservices/commands/list_dependencies.py +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/devservices/commands/list_services.py +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/devservices/commands/logs.py +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/devservices/commands/purge.py +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/devservices/commands/update.py +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/devservices/configs/service_config.py +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/devservices/utils/__init__.py +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/devservices/utils/check_for_update.py +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/devservices/utils/devenv.py +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/devservices/utils/docker.py +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/devservices/utils/file_lock.py +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/devservices/utils/git.py +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/devservices/utils/install_binary.py +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/devservices/utils/services.py +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/devservices.egg-info/dependency_links.txt +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/devservices.egg-info/entry_points.txt +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/devservices.egg-info/requires.txt +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/devservices.egg-info/top_level.txt +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/setup.cfg +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/testing/__init__.py +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/testing/utils.py +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/tests/__init__.py +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/tests/commands/test_down.py +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/tests/commands/test_list_dependencies.py +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/tests/commands/test_list_services.py +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/tests/commands/test_logs.py +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/tests/commands/test_purge.py +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/tests/commands/test_update.py +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/tests/configs/test_service_config.py +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/tests/conftest.py +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/tests/utils/test_check_for_update.py +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/tests/utils/test_docker.py +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/tests/utils/test_git.py +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/tests/utils/test_install_binary.py +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/tests/utils/test_services.py +0 -0
- {devservices-1.0.18 → devservices-1.1.0}/tests/utils/test_state.py +0 -0
|
@@ -30,7 +30,7 @@ NOTE: service-name is an optional parameter. If not provided, devservices will a
|
|
|
30
30
|
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.
|
|
31
31
|
|
|
32
32
|
```
|
|
33
|
-
devservices==1.0
|
|
33
|
+
devservices==1.1.0
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
### 2. Add devservices config files
|
|
@@ -22,6 +22,8 @@ from devservices.exceptions import ServiceNotFoundError
|
|
|
22
22
|
from devservices.utils.console import Console
|
|
23
23
|
from devservices.utils.console import Status
|
|
24
24
|
from devservices.utils.dependencies import construct_dependency_graph
|
|
25
|
+
from devservices.utils.dependencies import DependencyNode
|
|
26
|
+
from devservices.utils.dependencies import DependencyType
|
|
25
27
|
from devservices.utils.dependencies import get_non_shared_remote_dependencies
|
|
26
28
|
from devservices.utils.dependencies import install_and_verify_dependencies
|
|
27
29
|
from devservices.utils.dependencies import InstalledRemoteDependency
|
|
@@ -137,7 +139,9 @@ def down(args: Namespace) -> None:
|
|
|
137
139
|
# If no other service depends on the service we are trying to bring down, we can bring it down
|
|
138
140
|
if dependent_service_name is None:
|
|
139
141
|
try:
|
|
140
|
-
|
|
142
|
+
bring_down_service(
|
|
143
|
+
service, remote_dependencies, list(mode_dependencies), status
|
|
144
|
+
)
|
|
141
145
|
except DockerComposeError as dce:
|
|
142
146
|
capture_exception(dce, level="info")
|
|
143
147
|
status.failure(f"Failed to stop {service.name}: {dce.stderr}")
|
|
@@ -154,16 +158,7 @@ def down(args: Namespace) -> None:
|
|
|
154
158
|
console.success(f"{service.name} stopped")
|
|
155
159
|
|
|
156
160
|
|
|
157
|
-
def
|
|
158
|
-
cmd: DockerComposeCommand, current_env: dict[str, str], status: Status
|
|
159
|
-
) -> subprocess.CompletedProcess[str]:
|
|
160
|
-
# TODO: Get rid of these constants, we need a smarter way to determine the containers being brought down
|
|
161
|
-
for dependency in cmd.services:
|
|
162
|
-
status.info(f"Stopping {dependency}")
|
|
163
|
-
return run_cmd(cmd.full_command, current_env)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
def _down(
|
|
161
|
+
def bring_down_service(
|
|
167
162
|
service: Service,
|
|
168
163
|
remote_dependencies: set[InstalledRemoteDependency],
|
|
169
164
|
mode_dependencies: list[str],
|
|
@@ -234,7 +229,19 @@ def _get_dependent_service(
|
|
|
234
229
|
)
|
|
235
230
|
# If the service we are trying to bring down is in the dependency graph of another service,
|
|
236
231
|
# we should not bring it down
|
|
237
|
-
if
|
|
232
|
+
if (
|
|
233
|
+
DependencyNode(name=service.name, dependency_type=DependencyType.SERVICE)
|
|
234
|
+
in dependency_graph.graph
|
|
235
|
+
):
|
|
238
236
|
return other_started_service
|
|
239
237
|
|
|
240
238
|
return None
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _bring_down_dependency(
|
|
242
|
+
cmd: DockerComposeCommand, current_env: dict[str, str], status: Status
|
|
243
|
+
) -> subprocess.CompletedProcess[str]:
|
|
244
|
+
# TODO: Get rid of these constants, we need a smarter way to determine the containers being brought down
|
|
245
|
+
for dependency in cmd.services:
|
|
246
|
+
status.info(f"Stopping {dependency}")
|
|
247
|
+
return run_cmd(cmd.full_command, current_env)
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import concurrent.futures
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
from argparse import _SubParsersAction
|
|
8
|
+
from argparse import ArgumentParser
|
|
9
|
+
from argparse import Namespace
|
|
10
|
+
from collections import namedtuple
|
|
11
|
+
from typing import TypedDict
|
|
12
|
+
|
|
13
|
+
from sentry_sdk import capture_exception
|
|
14
|
+
|
|
15
|
+
from devservices.constants import Color
|
|
16
|
+
from devservices.constants import CONFIG_FILE_NAME
|
|
17
|
+
from devservices.constants import DEPENDENCY_CONFIG_VERSION
|
|
18
|
+
from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR
|
|
19
|
+
from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
|
|
20
|
+
from devservices.constants import DEVSERVICES_DIR_NAME
|
|
21
|
+
from devservices.exceptions import ConfigError
|
|
22
|
+
from devservices.exceptions import ConfigNotFoundError
|
|
23
|
+
from devservices.exceptions import DependencyError
|
|
24
|
+
from devservices.exceptions import DockerComposeError
|
|
25
|
+
from devservices.exceptions import ServiceNotFoundError
|
|
26
|
+
from devservices.utils.console import Console
|
|
27
|
+
from devservices.utils.dependencies import construct_dependency_graph
|
|
28
|
+
from devservices.utils.dependencies import DependencyGraph
|
|
29
|
+
from devservices.utils.dependencies import DependencyNode
|
|
30
|
+
from devservices.utils.dependencies import DependencyType
|
|
31
|
+
from devservices.utils.dependencies import install_and_verify_dependencies
|
|
32
|
+
from devservices.utils.dependencies import InstalledRemoteDependency
|
|
33
|
+
from devservices.utils.docker_compose import get_docker_compose_commands_to_run
|
|
34
|
+
from devservices.utils.docker_compose import run_cmd
|
|
35
|
+
from devservices.utils.services import find_matching_service
|
|
36
|
+
from devservices.utils.services import Service
|
|
37
|
+
from devservices.utils.state import ServiceRuntime
|
|
38
|
+
from devservices.utils.state import State
|
|
39
|
+
from devservices.utils.state import StateTables
|
|
40
|
+
|
|
41
|
+
BASE_INDENTATION = " "
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
ServiceStatus = namedtuple("ServiceStatus", ["name", "formatted_output"])
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ServiceStatusOutput(TypedDict):
|
|
48
|
+
Service: str
|
|
49
|
+
Name: str
|
|
50
|
+
State: str
|
|
51
|
+
Health: str
|
|
52
|
+
RunningFor: str
|
|
53
|
+
Publishers: list[Ports]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Ports(TypedDict):
|
|
57
|
+
URL: str
|
|
58
|
+
PublishedPort: int
|
|
59
|
+
TargetPort: int
|
|
60
|
+
Protocol: str
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
64
|
+
parser = subparsers.add_parser("status", help="View status of a service")
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"service_name",
|
|
67
|
+
help="Name of the service to view status for",
|
|
68
|
+
nargs="?",
|
|
69
|
+
default=None,
|
|
70
|
+
)
|
|
71
|
+
parser.set_defaults(func=status)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def status(args: Namespace) -> None:
|
|
75
|
+
"""Get the status of a specified service."""
|
|
76
|
+
console = Console()
|
|
77
|
+
service_name = args.service_name
|
|
78
|
+
try:
|
|
79
|
+
service = find_matching_service(service_name)
|
|
80
|
+
except ConfigNotFoundError as e:
|
|
81
|
+
capture_exception(e)
|
|
82
|
+
console.failure(
|
|
83
|
+
f"{str(e)}. Please specify a service (i.e. `devservices status sentry`) or run the command from a directory with a devservices configuration."
|
|
84
|
+
)
|
|
85
|
+
exit(1)
|
|
86
|
+
except ConfigError as e:
|
|
87
|
+
capture_exception(e)
|
|
88
|
+
console.failure(str(e))
|
|
89
|
+
exit(1)
|
|
90
|
+
except ServiceNotFoundError as e:
|
|
91
|
+
console.failure(str(e))
|
|
92
|
+
exit(1)
|
|
93
|
+
|
|
94
|
+
state = State()
|
|
95
|
+
starting_services = set(state.get_service_entries(StateTables.STARTING_SERVICES))
|
|
96
|
+
started_services = set(state.get_service_entries(StateTables.STARTED_SERVICES))
|
|
97
|
+
active_services = starting_services.union(started_services)
|
|
98
|
+
if service.name not in active_services:
|
|
99
|
+
console.warning(f"Status unavailable. {service.name} is not running standalone")
|
|
100
|
+
return # Since exit(0) is captured as an internal_error by sentry
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
status_tree = get_status_for_service(service)
|
|
104
|
+
except DependencyError as de:
|
|
105
|
+
capture_exception(de)
|
|
106
|
+
console.failure(
|
|
107
|
+
f"{str(de)}. If this error persists, try running `devservices purge`"
|
|
108
|
+
)
|
|
109
|
+
exit(1)
|
|
110
|
+
except DockerComposeError as dce:
|
|
111
|
+
capture_exception(dce)
|
|
112
|
+
console.failure(f"Failed to get status for {service.name}: {dce.stderr}")
|
|
113
|
+
exit(1)
|
|
114
|
+
console.info(status_tree)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def get_status_for_service(service: Service) -> str:
|
|
118
|
+
state = State()
|
|
119
|
+
|
|
120
|
+
modes = service.config.modes
|
|
121
|
+
|
|
122
|
+
starting_modes = set(
|
|
123
|
+
state.get_active_modes_for_service(service.name, StateTables.STARTING_SERVICES)
|
|
124
|
+
)
|
|
125
|
+
started_modes = set(
|
|
126
|
+
state.get_active_modes_for_service(service.name, StateTables.STARTED_SERVICES)
|
|
127
|
+
)
|
|
128
|
+
active_modes = starting_modes.union(started_modes)
|
|
129
|
+
mode_dependencies = set()
|
|
130
|
+
for active_mode in active_modes:
|
|
131
|
+
active_mode_dependencies = modes.get(active_mode, [])
|
|
132
|
+
mode_dependencies.update(active_mode_dependencies)
|
|
133
|
+
|
|
134
|
+
remote_dependencies = install_and_verify_dependencies(service)
|
|
135
|
+
|
|
136
|
+
dependency_graph = construct_dependency_graph(service, list(active_modes))
|
|
137
|
+
|
|
138
|
+
status_json_results = get_status_json_results(
|
|
139
|
+
service, remote_dependencies, list(mode_dependencies)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
docker_compose_service_to_status = parse_docker_compose_status(status_json_results)
|
|
143
|
+
status_tree = generate_service_status_tree(
|
|
144
|
+
service.name, dependency_graph, docker_compose_service_to_status
|
|
145
|
+
)
|
|
146
|
+
return status_tree
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def get_status_json_results(
|
|
150
|
+
service: Service,
|
|
151
|
+
remote_dependencies: set[InstalledRemoteDependency],
|
|
152
|
+
mode_dependencies: list[str],
|
|
153
|
+
) -> list[subprocess.CompletedProcess[str]]:
|
|
154
|
+
relative_local_dependency_directory = os.path.relpath(
|
|
155
|
+
os.path.join(DEVSERVICES_DEPENDENCIES_CACHE_DIR, DEPENDENCY_CONFIG_VERSION),
|
|
156
|
+
service.repo_path,
|
|
157
|
+
)
|
|
158
|
+
service_config_file_path = os.path.join(
|
|
159
|
+
service.repo_path, DEVSERVICES_DIR_NAME, CONFIG_FILE_NAME
|
|
160
|
+
)
|
|
161
|
+
# Set the environment variable for the local dependencies directory to be used by docker compose
|
|
162
|
+
current_env = os.environ.copy()
|
|
163
|
+
current_env[
|
|
164
|
+
DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
|
|
165
|
+
] = relative_local_dependency_directory
|
|
166
|
+
docker_compose_commands = get_docker_compose_commands_to_run(
|
|
167
|
+
service=service,
|
|
168
|
+
remote_dependencies=list(remote_dependencies),
|
|
169
|
+
current_env=current_env,
|
|
170
|
+
command="ps",
|
|
171
|
+
options=["--format", "json"],
|
|
172
|
+
service_config_file_path=service_config_file_path,
|
|
173
|
+
mode_dependencies=mode_dependencies,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
cmd_outputs = []
|
|
177
|
+
|
|
178
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
179
|
+
futures = [
|
|
180
|
+
executor.submit(run_cmd, cmd.full_command, current_env)
|
|
181
|
+
for cmd in docker_compose_commands
|
|
182
|
+
]
|
|
183
|
+
for future in concurrent.futures.as_completed(futures):
|
|
184
|
+
cmd_outputs.append(future.result())
|
|
185
|
+
|
|
186
|
+
return cmd_outputs
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def generate_service_status_tree(
|
|
190
|
+
service_name: str,
|
|
191
|
+
dependency_graph: DependencyGraph,
|
|
192
|
+
docker_compose_service_to_status: dict[str, ServiceStatusOutput],
|
|
193
|
+
indentation: str = "",
|
|
194
|
+
) -> str:
|
|
195
|
+
output = []
|
|
196
|
+
state = State()
|
|
197
|
+
services_with_local_runtime = state.get_services_by_runtime(ServiceRuntime.LOCAL)
|
|
198
|
+
|
|
199
|
+
dependencies = dependency_graph.graph[
|
|
200
|
+
DependencyNode(name=service_name, dependency_type=DependencyType.SERVICE)
|
|
201
|
+
]
|
|
202
|
+
|
|
203
|
+
# Using indentation == "" to check if the service is the root service (hacky, but works) since the root service may not be in the services_with_local_runtime set
|
|
204
|
+
runtime = (
|
|
205
|
+
"local"
|
|
206
|
+
if service_name in services_with_local_runtime or indentation == ""
|
|
207
|
+
else "containerized"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
output = [
|
|
211
|
+
f"{indentation}{Color.BOLD}{service_name}{Color.RESET}:",
|
|
212
|
+
f"{indentation}{BASE_INDENTATION}Type: service",
|
|
213
|
+
f"{indentation}{BASE_INDENTATION}Runtime: {runtime}",
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
for dependency in sorted(
|
|
217
|
+
dependencies, key=lambda d: (d.dependency_type.value, d.name)
|
|
218
|
+
):
|
|
219
|
+
if dependency.name in services_with_local_runtime:
|
|
220
|
+
output.append(
|
|
221
|
+
process_service_with_local_runtime(
|
|
222
|
+
dependency,
|
|
223
|
+
indentation + BASE_INDENTATION,
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
else:
|
|
227
|
+
output.append(
|
|
228
|
+
process_service_with_containerized_runtime(
|
|
229
|
+
dependency,
|
|
230
|
+
docker_compose_service_to_status,
|
|
231
|
+
indentation + BASE_INDENTATION,
|
|
232
|
+
dependency_graph,
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
return "\n".join(output)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def process_service_with_local_runtime(
|
|
239
|
+
dependency: DependencyNode,
|
|
240
|
+
indentation: str,
|
|
241
|
+
) -> str:
|
|
242
|
+
output = []
|
|
243
|
+
state = State()
|
|
244
|
+
starting_services = set(state.get_service_entries(StateTables.STARTING_SERVICES))
|
|
245
|
+
started_services = set(state.get_service_entries(StateTables.STARTED_SERVICES))
|
|
246
|
+
|
|
247
|
+
if dependency.name in started_services:
|
|
248
|
+
return handle_started_service(dependency, indentation)
|
|
249
|
+
elif dependency.name in starting_services:
|
|
250
|
+
output.append(f"{indentation}{Color.BOLD}{dependency.name}{Color.RESET}:")
|
|
251
|
+
output.append(f"{indentation}{BASE_INDENTATION}Type: service")
|
|
252
|
+
output.append(f"{indentation}{BASE_INDENTATION}Status: starting")
|
|
253
|
+
output.append(f"{indentation}{BASE_INDENTATION}Runtime: local")
|
|
254
|
+
else:
|
|
255
|
+
output.append(f"{indentation}{Color.BOLD}{dependency.name}{Color.RESET}:")
|
|
256
|
+
output.append(f"{indentation}{BASE_INDENTATION}Type: service")
|
|
257
|
+
output.append(f"{indentation}{BASE_INDENTATION}Status: N/A")
|
|
258
|
+
output.append(f"{indentation}{BASE_INDENTATION}Runtime: local")
|
|
259
|
+
return "\n".join(output)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def process_service_with_containerized_runtime(
|
|
263
|
+
dependency: DependencyNode,
|
|
264
|
+
docker_compose_service_to_status: dict[str, ServiceStatusOutput],
|
|
265
|
+
indentation: str,
|
|
266
|
+
dependency_graph: DependencyGraph,
|
|
267
|
+
) -> str:
|
|
268
|
+
if len(dependency_graph.graph[dependency]) > 0:
|
|
269
|
+
return generate_service_status_tree(
|
|
270
|
+
dependency.name,
|
|
271
|
+
dependency_graph,
|
|
272
|
+
docker_compose_service_to_status,
|
|
273
|
+
indentation,
|
|
274
|
+
)
|
|
275
|
+
else:
|
|
276
|
+
return generate_service_status_details(
|
|
277
|
+
dependency, docker_compose_service_to_status, indentation
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def parse_docker_compose_status(
|
|
282
|
+
status_json_results: list[subprocess.CompletedProcess[str]],
|
|
283
|
+
) -> dict[str, ServiceStatusOutput]:
|
|
284
|
+
"""Parse the JSON output from docker-compose status command."""
|
|
285
|
+
docker_compose_service_to_status: dict[str, ServiceStatusOutput] = {}
|
|
286
|
+
for status_json in status_json_results:
|
|
287
|
+
if not status_json.stdout:
|
|
288
|
+
continue
|
|
289
|
+
docker_compose_service_status_output = status_json.stdout.split("\n")[:-1]
|
|
290
|
+
for docker_compose_service_status in docker_compose_service_status_output:
|
|
291
|
+
docker_compose_service_status_json = json.loads(
|
|
292
|
+
docker_compose_service_status
|
|
293
|
+
)
|
|
294
|
+
compose_service = docker_compose_service_status_json["Service"]
|
|
295
|
+
docker_compose_service_to_status[
|
|
296
|
+
compose_service
|
|
297
|
+
] = docker_compose_service_status_json
|
|
298
|
+
|
|
299
|
+
return docker_compose_service_to_status
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def generate_service_status_details(
|
|
303
|
+
dependency: DependencyNode,
|
|
304
|
+
docker_compose_service_to_status: dict[str, ServiceStatusOutput],
|
|
305
|
+
indentation: str,
|
|
306
|
+
) -> str:
|
|
307
|
+
output = [f"{indentation}{Color.BOLD}{dependency.name}{Color.RESET}:"]
|
|
308
|
+
|
|
309
|
+
if dependency.name not in docker_compose_service_to_status:
|
|
310
|
+
return "\n".join(
|
|
311
|
+
[
|
|
312
|
+
*output,
|
|
313
|
+
f"{indentation}{BASE_INDENTATION}Type: container",
|
|
314
|
+
f"{indentation}{BASE_INDENTATION}Status: N/A",
|
|
315
|
+
]
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
service_status = docker_compose_service_to_status[dependency.name]
|
|
319
|
+
details = [
|
|
320
|
+
"Type: container",
|
|
321
|
+
f"Status: {service_status.get('State', 'N/A')}",
|
|
322
|
+
f"Health: {format_health(service_status.get('Health', 'N/A'))}",
|
|
323
|
+
f"Container: {service_status.get('Name', 'N/A')}",
|
|
324
|
+
f"Uptime: {service_status.get('RunningFor', 'N/A')}",
|
|
325
|
+
]
|
|
326
|
+
|
|
327
|
+
output.extend(f"{indentation}{BASE_INDENTATION}{detail}" for detail in details)
|
|
328
|
+
|
|
329
|
+
if service_ports := service_status.get("Publishers", []):
|
|
330
|
+
output.append(f"{indentation}{BASE_INDENTATION}Ports:")
|
|
331
|
+
for service_port in service_ports:
|
|
332
|
+
output.append(
|
|
333
|
+
f"{indentation}{BASE_INDENTATION}{BASE_INDENTATION}{service_port['URL']}:{service_port['PublishedPort']} -> {service_port['TargetPort']}/{service_port['Protocol']}"
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
return "\n".join(output)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def handle_started_service(dependency: DependencyNode, indentation: str) -> str:
|
|
340
|
+
try:
|
|
341
|
+
service_with_local_runtime = find_matching_service(dependency.name)
|
|
342
|
+
except (ConfigError, ServiceNotFoundError) as e:
|
|
343
|
+
capture_exception(e)
|
|
344
|
+
return "\n".join(
|
|
345
|
+
[
|
|
346
|
+
f"{indentation}{Color.BOLD}{dependency.name}{Color.RESET}:",
|
|
347
|
+
f"{indentation}{BASE_INDENTATION}Type: service",
|
|
348
|
+
f"{indentation}{BASE_INDENTATION}Status: N/A",
|
|
349
|
+
f"{indentation}{BASE_INDENTATION}Runtime: local",
|
|
350
|
+
]
|
|
351
|
+
)
|
|
352
|
+
service_output = get_status_for_service(service_with_local_runtime)
|
|
353
|
+
return "\n".join(
|
|
354
|
+
[f"{indentation}{line}" for line in service_output.splitlines()],
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def format_health(health: str) -> str:
|
|
359
|
+
"""Format the health status for display."""
|
|
360
|
+
color = (
|
|
361
|
+
Color.GREEN
|
|
362
|
+
if health.lower() == "healthy"
|
|
363
|
+
else Color.RED
|
|
364
|
+
if health.lower() == "unhealthy"
|
|
365
|
+
else Color.YELLOW
|
|
366
|
+
)
|
|
367
|
+
return f"{color}{health}{Color.RESET}"
|