devservices 1.1.1__tar.gz → 1.1.2__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.1.1 → devservices-1.1.2}/PKG-INFO +2 -2
- {devservices-1.1.1 → devservices-1.1.2}/README.md +1 -1
- devservices-1.1.2/devenv/sync.py +24 -0
- {devservices-1.1.1 → devservices-1.1.2}/devservices/commands/down.py +56 -16
- devservices-1.1.2/devservices/commands/serve.py +68 -0
- {devservices-1.1.1 → devservices-1.1.2}/devservices/commands/toggle.py +11 -6
- {devservices-1.1.1 → devservices-1.1.2}/devservices/commands/up.py +33 -9
- {devservices-1.1.1 → devservices-1.1.2}/devservices/constants.py +1 -0
- {devservices-1.1.1 → devservices-1.1.2}/devservices/main.py +2 -0
- {devservices-1.1.1 → devservices-1.1.2}/devservices/utils/dependencies.py +14 -4
- {devservices-1.1.1 → devservices-1.1.2}/devservices/utils/supervisor.py +20 -8
- {devservices-1.1.1 → devservices-1.1.2}/devservices.egg-info/PKG-INFO +2 -2
- {devservices-1.1.1 → devservices-1.1.2}/devservices.egg-info/SOURCES.txt +3 -0
- {devservices-1.1.1 → devservices-1.1.2}/devservices.egg-info/top_level.txt +1 -0
- {devservices-1.1.1 → devservices-1.1.2}/pyproject.toml +3 -3
- {devservices-1.1.1 → devservices-1.1.2}/testing/utils.py +9 -0
- {devservices-1.1.1 → devservices-1.1.2}/tests/commands/test_down.py +56 -40
- devservices-1.1.2/tests/commands/test_serve.py +163 -0
- {devservices-1.1.1 → devservices-1.1.2}/tests/commands/test_toggle.py +39 -16
- {devservices-1.1.1 → devservices-1.1.2}/tests/commands/test_up.py +445 -128
- {devservices-1.1.1 → devservices-1.1.2}/tests/utils/test_dependencies.py +177 -6
- {devservices-1.1.1 → devservices-1.1.2}/tests/utils/test_supervisor.py +30 -12
- {devservices-1.1.1 → devservices-1.1.2}/LICENSE.md +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/devservices/__init__.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/devservices/commands/__init__.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/devservices/commands/list_dependencies.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/devservices/commands/list_services.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/devservices/commands/logs.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/devservices/commands/purge.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/devservices/commands/status.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/devservices/commands/update.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/devservices/configs/service_config.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/devservices/exceptions.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/devservices/utils/__init__.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/devservices/utils/check_for_update.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/devservices/utils/console.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/devservices/utils/devenv.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/devservices/utils/docker.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/devservices/utils/docker_compose.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/devservices/utils/file_lock.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/devservices/utils/git.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/devservices/utils/install_binary.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/devservices/utils/services.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/devservices/utils/state.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/devservices.egg-info/dependency_links.txt +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/devservices.egg-info/entry_points.txt +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/devservices.egg-info/requires.txt +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/setup.cfg +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/testing/__init__.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/tests/__init__.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/tests/commands/test_list_dependencies.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/tests/commands/test_list_services.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/tests/commands/test_logs.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/tests/commands/test_purge.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/tests/commands/test_status.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/tests/commands/test_update.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/tests/configs/test_service_config.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/tests/conftest.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/tests/utils/test_check_for_update.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/tests/utils/test_docker.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/tests/utils/test_docker_compose.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/tests/utils/test_git.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/tests/utils/test_install_binary.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/tests/utils/test_services.py +0 -0
- {devservices-1.1.1 → devservices-1.1.2}/tests/utils/test_state.py +0 -0
|
@@ -31,7 +31,7 @@ NOTE: service-name is an optional parameter. If not provided, devservices will a
|
|
|
31
31
|
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
32
|
|
|
33
33
|
```
|
|
34
|
-
devservices==1.1.
|
|
34
|
+
devservices==1.1.2
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
### 2. Add devservices config files
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from devenv.lib import config
|
|
4
|
+
from devenv.lib import proc
|
|
5
|
+
from devenv.lib import venv
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main(context: dict[str, str]) -> int:
|
|
9
|
+
reporoot = context["reporoot"]
|
|
10
|
+
|
|
11
|
+
venv_dir, python_version, requirements, editable_paths, bins = venv.get(
|
|
12
|
+
reporoot,
|
|
13
|
+
"venv",
|
|
14
|
+
)
|
|
15
|
+
url, sha256 = config.get_python(reporoot, python_version)
|
|
16
|
+
print(f"ensuring venv at {venv_dir}...")
|
|
17
|
+
venv.ensure(venv_dir, python_version, url, sha256)
|
|
18
|
+
|
|
19
|
+
print(f"syncing venv with {requirements}...")
|
|
20
|
+
venv.sync(reporoot, venv_dir, requirements, editable_paths, bins)
|
|
21
|
+
|
|
22
|
+
proc.run(("pre-commit", "install", "--install-hooks", "-f"))
|
|
23
|
+
|
|
24
|
+
return 0
|
|
@@ -53,6 +53,12 @@ def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
|
53
53
|
action="store_true",
|
|
54
54
|
default=False,
|
|
55
55
|
)
|
|
56
|
+
parser.add_argument(
|
|
57
|
+
"--exclude-local",
|
|
58
|
+
help="Exclude dependencies with local runtime from being brought down",
|
|
59
|
+
action="store_true",
|
|
60
|
+
default=False,
|
|
61
|
+
)
|
|
56
62
|
parser.set_defaults(func=down)
|
|
57
63
|
|
|
58
64
|
|
|
@@ -77,6 +83,7 @@ def down(args: Namespace) -> None:
|
|
|
77
83
|
exit(1)
|
|
78
84
|
|
|
79
85
|
modes = service.config.modes
|
|
86
|
+
exclude_local = args.exclude_local
|
|
80
87
|
|
|
81
88
|
state = State()
|
|
82
89
|
starting_services = set(state.get_service_entries(StateTables.STARTING_SERVICES))
|
|
@@ -113,7 +120,7 @@ def down(args: Namespace) -> None:
|
|
|
113
120
|
exit(1)
|
|
114
121
|
try:
|
|
115
122
|
remote_dependencies = get_non_shared_remote_dependencies(
|
|
116
|
-
service, remote_dependencies
|
|
123
|
+
service, remote_dependencies, exclude_local
|
|
117
124
|
)
|
|
118
125
|
except DependencyError as de:
|
|
119
126
|
capture_exception(de)
|
|
@@ -125,13 +132,13 @@ def down(args: Namespace) -> None:
|
|
|
125
132
|
# Check if any service depends on the service we are trying to bring down
|
|
126
133
|
# TODO: We should also take into account the active modes of the other services (this is not trivial to do)
|
|
127
134
|
other_started_services = active_services.difference({service.name})
|
|
128
|
-
|
|
135
|
+
services_with_local_runtimes = state.get_services_by_runtime(
|
|
129
136
|
ServiceRuntime.LOCAL
|
|
130
137
|
)
|
|
131
138
|
dependent_service_name = None
|
|
132
139
|
# We can ignore checking if anything relies on the service
|
|
133
140
|
# if it is a locally running service
|
|
134
|
-
if service.name not in
|
|
141
|
+
if service.name not in services_with_local_runtimes:
|
|
135
142
|
dependent_service_name = _get_dependent_service(
|
|
136
143
|
service, other_started_services, state
|
|
137
144
|
)
|
|
@@ -140,7 +147,11 @@ def down(args: Namespace) -> None:
|
|
|
140
147
|
if dependent_service_name is None:
|
|
141
148
|
try:
|
|
142
149
|
bring_down_service(
|
|
143
|
-
service,
|
|
150
|
+
service,
|
|
151
|
+
remote_dependencies,
|
|
152
|
+
list(mode_dependencies),
|
|
153
|
+
exclude_local,
|
|
154
|
+
status,
|
|
144
155
|
)
|
|
145
156
|
except DockerComposeError as dce:
|
|
146
157
|
capture_exception(dce, level="info")
|
|
@@ -154,6 +165,24 @@ def down(args: Namespace) -> None:
|
|
|
154
165
|
# TODO: We should factor in healthchecks here before marking service as not running
|
|
155
166
|
state.remove_service_entry(service.name, StateTables.STARTING_SERVICES)
|
|
156
167
|
state.remove_service_entry(service.name, StateTables.STARTED_SERVICES)
|
|
168
|
+
|
|
169
|
+
dependencies_with_local_runtimes = set()
|
|
170
|
+
for service_with_local_runtime in services_with_local_runtimes:
|
|
171
|
+
if service_with_local_runtime in {
|
|
172
|
+
dep.service_name for dep in remote_dependencies
|
|
173
|
+
}:
|
|
174
|
+
dependencies_with_local_runtimes.add(service_with_local_runtime)
|
|
175
|
+
|
|
176
|
+
active_dependencies_with_local_runtimes = set()
|
|
177
|
+
for dependency_with_local_runtime in dependencies_with_local_runtimes:
|
|
178
|
+
if dependency_with_local_runtime in active_services:
|
|
179
|
+
active_dependencies_with_local_runtimes.add(dependency_with_local_runtime)
|
|
180
|
+
|
|
181
|
+
if not exclude_local and len(active_dependencies_with_local_runtimes) > 0:
|
|
182
|
+
status.warning("Stopping dependencies with local runtimes...")
|
|
183
|
+
for local_dependency in active_dependencies_with_local_runtimes:
|
|
184
|
+
down(Namespace(service_name=local_dependency, exclude_local=exclude_local))
|
|
185
|
+
|
|
157
186
|
if dependent_service_name is None:
|
|
158
187
|
console.success(f"{service.name} stopped")
|
|
159
188
|
|
|
@@ -162,6 +191,7 @@ def bring_down_service(
|
|
|
162
191
|
service: Service,
|
|
163
192
|
remote_dependencies: set[InstalledRemoteDependency],
|
|
164
193
|
mode_dependencies: list[str],
|
|
194
|
+
exclude_local: bool,
|
|
165
195
|
status: Status,
|
|
166
196
|
) -> None:
|
|
167
197
|
relative_local_dependency_directory = os.path.relpath(
|
|
@@ -171,30 +201,40 @@ def bring_down_service(
|
|
|
171
201
|
service_config_file_path = os.path.join(
|
|
172
202
|
service.repo_path, DEVSERVICES_DIR_NAME, CONFIG_FILE_NAME
|
|
173
203
|
)
|
|
204
|
+
|
|
174
205
|
# Set the environment variable for the local dependencies directory to be used by docker compose
|
|
175
206
|
current_env = os.environ.copy()
|
|
176
207
|
current_env[
|
|
177
208
|
DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
|
|
178
209
|
] = relative_local_dependency_directory
|
|
179
210
|
state = State()
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
211
|
+
|
|
212
|
+
# We want to ignore any dependencies that are set to run locally if we are excluding local dependencies
|
|
213
|
+
services_with_local_runtimes = state.get_services_by_runtime(ServiceRuntime.LOCAL)
|
|
214
|
+
|
|
215
|
+
dependencies_with_local_runtimes = set()
|
|
216
|
+
for service_with_local_runtime in services_with_local_runtimes:
|
|
217
|
+
if service_with_local_runtime in {
|
|
218
|
+
dep.service_name for dep in remote_dependencies
|
|
219
|
+
}:
|
|
220
|
+
dependencies_with_local_runtimes.add(service_with_local_runtime)
|
|
221
|
+
|
|
190
222
|
docker_compose_commands = get_docker_compose_commands_to_run(
|
|
191
223
|
service=service,
|
|
192
|
-
remote_dependencies=
|
|
224
|
+
remote_dependencies=[
|
|
225
|
+
dep
|
|
226
|
+
for dep in remote_dependencies
|
|
227
|
+
if dep.service_name not in dependencies_with_local_runtimes
|
|
228
|
+
],
|
|
193
229
|
current_env=current_env,
|
|
194
230
|
command="stop",
|
|
195
231
|
options=[],
|
|
196
232
|
service_config_file_path=service_config_file_path,
|
|
197
|
-
mode_dependencies=
|
|
233
|
+
mode_dependencies=[
|
|
234
|
+
dep
|
|
235
|
+
for dep in mode_dependencies
|
|
236
|
+
if dep not in dependencies_with_local_runtimes
|
|
237
|
+
],
|
|
198
238
|
)
|
|
199
239
|
|
|
200
240
|
cmd_outputs = []
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import pty
|
|
5
|
+
import shlex
|
|
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 DEVSERVICES_DIR_NAME
|
|
13
|
+
from devservices.constants import PROGRAMS_CONF_FILE_NAME
|
|
14
|
+
from devservices.exceptions import ConfigError
|
|
15
|
+
from devservices.exceptions import ConfigNotFoundError
|
|
16
|
+
from devservices.exceptions import SupervisorConfigError
|
|
17
|
+
from devservices.utils.console import Console
|
|
18
|
+
from devservices.utils.services import find_matching_service
|
|
19
|
+
from devservices.utils.supervisor import SupervisorManager
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
23
|
+
# prefix_chars is a hack to allow all options to be passed through to the devserver without argparse complaining
|
|
24
|
+
parser = subparsers.add_parser(
|
|
25
|
+
"serve", help="Serve the devserver", prefix_chars="+"
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"extra", nargs="*", help="Flags to pass through to the devserver"
|
|
29
|
+
)
|
|
30
|
+
parser.set_defaults(func=serve)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def serve(args: Namespace) -> None:
|
|
34
|
+
"""Serve the devserver."""
|
|
35
|
+
console = Console()
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
service = find_matching_service()
|
|
39
|
+
except ConfigNotFoundError as e:
|
|
40
|
+
console.failure(
|
|
41
|
+
f"{str(e)}. Please run the command from a directory with a valid devservices configuration."
|
|
42
|
+
)
|
|
43
|
+
return
|
|
44
|
+
except ConfigError as e:
|
|
45
|
+
capture_exception(e)
|
|
46
|
+
console.failure(str(e))
|
|
47
|
+
exit(1)
|
|
48
|
+
|
|
49
|
+
programs_config_path = os.path.join(
|
|
50
|
+
service.repo_path, f"{DEVSERVICES_DIR_NAME}/{PROGRAMS_CONF_FILE_NAME}"
|
|
51
|
+
)
|
|
52
|
+
if not os.path.exists(programs_config_path):
|
|
53
|
+
console.failure(f"No programs.conf file found in {programs_config_path}.")
|
|
54
|
+
return
|
|
55
|
+
manager = SupervisorManager(
|
|
56
|
+
programs_config_path,
|
|
57
|
+
service_name=service.name,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
devserver_command = manager.get_program_command("devserver")
|
|
62
|
+
except SupervisorConfigError as e:
|
|
63
|
+
capture_exception(e, level="info")
|
|
64
|
+
console.failure(f"Error when getting devserver command: {str(e)}")
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
argv = shlex.split(devserver_command) + args.extra
|
|
68
|
+
pty.spawn(argv)
|
|
@@ -40,7 +40,7 @@ def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
|
40
40
|
parser.add_argument(
|
|
41
41
|
"runtime",
|
|
42
42
|
help="Runtime to use for the service",
|
|
43
|
-
choices=[
|
|
43
|
+
choices=[runtime.value for runtime in ServiceRuntime],
|
|
44
44
|
nargs="?",
|
|
45
45
|
default=None,
|
|
46
46
|
)
|
|
@@ -71,7 +71,7 @@ def toggle(args: Namespace) -> None:
|
|
|
71
71
|
state = State()
|
|
72
72
|
current_runtime = state.get_service_runtime(service.name)
|
|
73
73
|
if desired_runtime is None:
|
|
74
|
-
desired_runtime = get_opposite_runtime(current_runtime)
|
|
74
|
+
desired_runtime = get_opposite_runtime(current_runtime).value
|
|
75
75
|
if current_runtime == desired_runtime:
|
|
76
76
|
console.warning(
|
|
77
77
|
f"{service.name} is already running in {desired_runtime} runtime"
|
|
@@ -105,7 +105,7 @@ def handle_transition_to_local_runtime(service_to_transition: Service) -> None:
|
|
|
105
105
|
if service_to_transition.name in active_services:
|
|
106
106
|
state.update_service_runtime(service_to_transition.name, ServiceRuntime.LOCAL)
|
|
107
107
|
console.success(
|
|
108
|
-
f"{service_to_transition.name} is now running in {ServiceRuntime.LOCAL} runtime"
|
|
108
|
+
f"{service_to_transition.name} is now running in {ServiceRuntime.LOCAL.value} runtime"
|
|
109
109
|
)
|
|
110
110
|
return
|
|
111
111
|
|
|
@@ -196,7 +196,7 @@ def restart_dependent_services(
|
|
|
196
196
|
console = Console()
|
|
197
197
|
with Status(
|
|
198
198
|
on_start=lambda: console.warning(
|
|
199
|
-
f"Restarting dependent services to ensure {service_name} is running in a {ServiceRuntime.CONTAINERIZED} runtime"
|
|
199
|
+
f"Restarting dependent services to ensure {service_name} is running in a {ServiceRuntime.CONTAINERIZED.value} runtime"
|
|
200
200
|
),
|
|
201
201
|
) as status:
|
|
202
202
|
for dependent_service in dependent_services:
|
|
@@ -223,6 +223,7 @@ def bring_down_containerized_service(
|
|
|
223
223
|
) -> None:
|
|
224
224
|
"""Bring down a containerized service running within another service."""
|
|
225
225
|
console = Console()
|
|
226
|
+
exclude_local = True
|
|
226
227
|
with Status(
|
|
227
228
|
lambda: console.warning(f"Stopping {service.name}"),
|
|
228
229
|
) as status:
|
|
@@ -242,7 +243,7 @@ def bring_down_containerized_service(
|
|
|
242
243
|
exit(1)
|
|
243
244
|
try:
|
|
244
245
|
remote_dependencies = get_non_shared_remote_dependencies(
|
|
245
|
-
service, remote_dependencies
|
|
246
|
+
service, remote_dependencies, exclude_local
|
|
246
247
|
)
|
|
247
248
|
except DependencyError as de:
|
|
248
249
|
capture_exception(de)
|
|
@@ -252,7 +253,11 @@ def bring_down_containerized_service(
|
|
|
252
253
|
exit(1)
|
|
253
254
|
try:
|
|
254
255
|
bring_down_service(
|
|
255
|
-
service,
|
|
256
|
+
service,
|
|
257
|
+
remote_dependencies,
|
|
258
|
+
sorted(list(mode_dependencies)),
|
|
259
|
+
exclude_local,
|
|
260
|
+
status,
|
|
256
261
|
)
|
|
257
262
|
except DockerComposeError as dce:
|
|
258
263
|
capture_exception(dce, level="info")
|
|
@@ -56,10 +56,16 @@ def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
|
56
56
|
help="Mode to use for the service",
|
|
57
57
|
default="default",
|
|
58
58
|
)
|
|
59
|
+
parser.add_argument(
|
|
60
|
+
"--exclude-local",
|
|
61
|
+
help="Exclude dependencies with local runtime from being started",
|
|
62
|
+
action="store_true",
|
|
63
|
+
default=False,
|
|
64
|
+
)
|
|
59
65
|
parser.set_defaults(func=up)
|
|
60
66
|
|
|
61
67
|
|
|
62
|
-
def up(args: Namespace) -> None:
|
|
68
|
+
def up(args: Namespace, existing_status: Status | None = None) -> None:
|
|
63
69
|
"""Bring up a service and its dependencies."""
|
|
64
70
|
console = Console()
|
|
65
71
|
service_name = args.service_name
|
|
@@ -85,8 +91,12 @@ def up(args: Namespace) -> None:
|
|
|
85
91
|
state = State()
|
|
86
92
|
|
|
87
93
|
with Status(
|
|
88
|
-
lambda: console.warning(f"Starting '{service.name}' in mode: '{mode}'")
|
|
89
|
-
|
|
94
|
+
lambda: console.warning(f"Starting '{service.name}' in mode: '{mode}'")
|
|
95
|
+
if existing_status is None
|
|
96
|
+
else existing_status.warning(f"Starting '{service.name}' in mode: '{mode}'"),
|
|
97
|
+
lambda: console.success(f"{service.name} started")
|
|
98
|
+
if existing_status is None
|
|
99
|
+
else existing_status.success(f"{service.name} started"),
|
|
90
100
|
) as status:
|
|
91
101
|
services_with_local_runtime = state.get_services_by_runtime(
|
|
92
102
|
ServiceRuntime.LOCAL
|
|
@@ -99,9 +109,10 @@ def up(args: Namespace) -> None:
|
|
|
99
109
|
and service_with_local_runtime in modes[mode]
|
|
100
110
|
):
|
|
101
111
|
skipped_services.add(service_with_local_runtime)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
112
|
+
if args.exclude_local:
|
|
113
|
+
status.warning(
|
|
114
|
+
f"Skipping '{service_with_local_runtime}' as it is set to run locally"
|
|
115
|
+
)
|
|
105
116
|
try:
|
|
106
117
|
status.info("Retrieving dependencies")
|
|
107
118
|
remote_dependencies = install_and_verify_dependencies(
|
|
@@ -131,9 +142,10 @@ def up(args: Namespace) -> None:
|
|
|
131
142
|
and service_with_local_runtime not in skipped_services
|
|
132
143
|
):
|
|
133
144
|
skipped_services.add(service_with_local_runtime)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
145
|
+
if args.exclude_local:
|
|
146
|
+
status.warning(
|
|
147
|
+
f"Skipping '{service_with_local_runtime}' as it is set to run locally"
|
|
148
|
+
)
|
|
137
149
|
# We want to ignore any dependencies that are set to run locally
|
|
138
150
|
mode_dependencies = [
|
|
139
151
|
dep for dep in mode_dependencies if dep not in services_with_local_runtime
|
|
@@ -144,6 +156,18 @@ def up(args: Namespace) -> None:
|
|
|
144
156
|
if dep.service_name not in services_with_local_runtime
|
|
145
157
|
}
|
|
146
158
|
try:
|
|
159
|
+
if not args.exclude_local:
|
|
160
|
+
status.warning("Starting dependencies with local runtimes...")
|
|
161
|
+
for skipped_service in skipped_services:
|
|
162
|
+
up(
|
|
163
|
+
Namespace(
|
|
164
|
+
service_name=skipped_service,
|
|
165
|
+
mode=mode,
|
|
166
|
+
exclude_local=True,
|
|
167
|
+
),
|
|
168
|
+
status,
|
|
169
|
+
)
|
|
170
|
+
status.warning(f"Continuing with service '{service.name}'")
|
|
147
171
|
_up(service, [mode], remote_dependencies, mode_dependencies, status)
|
|
148
172
|
except DockerComposeError as dce:
|
|
149
173
|
capture_exception(dce, level="info")
|
|
@@ -18,6 +18,7 @@ class Color:
|
|
|
18
18
|
MINIMUM_DOCKER_COMPOSE_VERSION = "2.29.7"
|
|
19
19
|
DEVSERVICES_DIR_NAME = "devservices"
|
|
20
20
|
CONFIG_FILE_NAME = "config.yml"
|
|
21
|
+
PROGRAMS_CONF_FILE_NAME = "programs.conf"
|
|
21
22
|
DOCKER_CONFIG_DIR = os.environ.get("DOCKER_CONFIG", os.path.expanduser("~/.docker"))
|
|
22
23
|
DOCKER_USER_PLUGIN_DIR = os.path.join(DOCKER_CONFIG_DIR, "cli-plugins/")
|
|
23
24
|
|
|
@@ -24,6 +24,7 @@ from devservices.commands import list_dependencies
|
|
|
24
24
|
from devservices.commands import list_services
|
|
25
25
|
from devservices.commands import logs
|
|
26
26
|
from devservices.commands import purge
|
|
27
|
+
from devservices.commands import serve
|
|
27
28
|
from devservices.commands import status
|
|
28
29
|
from devservices.commands import toggle
|
|
29
30
|
from devservices.commands import up
|
|
@@ -147,6 +148,7 @@ def main() -> None:
|
|
|
147
148
|
logs.add_parser(subparsers)
|
|
148
149
|
update.add_parser(subparsers)
|
|
149
150
|
purge.add_parser(subparsers)
|
|
151
|
+
serve.add_parser(subparsers)
|
|
150
152
|
toggle.add_parser(subparsers)
|
|
151
153
|
|
|
152
154
|
args = parser.parse_args()
|
|
@@ -39,6 +39,7 @@ from devservices.exceptions import UnableToCloneDependencyError
|
|
|
39
39
|
from devservices.utils.file_lock import lock
|
|
40
40
|
from devservices.utils.services import find_matching_service
|
|
41
41
|
from devservices.utils.services import Service
|
|
42
|
+
from devservices.utils.state import ServiceRuntime
|
|
42
43
|
from devservices.utils.state import State
|
|
43
44
|
from devservices.utils.state import StateTables
|
|
44
45
|
|
|
@@ -279,7 +280,9 @@ def verify_local_dependencies(dependencies: list[Dependency]) -> bool:
|
|
|
279
280
|
|
|
280
281
|
|
|
281
282
|
def get_non_shared_remote_dependencies(
|
|
282
|
-
service_to_stop: Service,
|
|
283
|
+
service_to_stop: Service,
|
|
284
|
+
remote_dependencies: set[InstalledRemoteDependency],
|
|
285
|
+
exclude_local: bool,
|
|
283
286
|
) -> set[InstalledRemoteDependency]:
|
|
284
287
|
state = State()
|
|
285
288
|
starting_services = set(state.get_service_entries(StateTables.STARTING_SERVICES))
|
|
@@ -291,6 +294,7 @@ def get_non_shared_remote_dependencies(
|
|
|
291
294
|
|
|
292
295
|
active_modes: dict[str, list[str]] = dict()
|
|
293
296
|
for active_service in active_services:
|
|
297
|
+
# TODO: We probably shouldn't use an OR here, but an AND
|
|
294
298
|
starting_modes = state.get_active_modes_for_service(
|
|
295
299
|
active_service, StateTables.STARTING_SERVICES
|
|
296
300
|
)
|
|
@@ -303,9 +307,15 @@ def get_non_shared_remote_dependencies(
|
|
|
303
307
|
base_running_service_names: set[str] = set()
|
|
304
308
|
for started_service_name in active_services:
|
|
305
309
|
started_service = find_matching_service(started_service_name)
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
310
|
+
started_service_runtime = state.get_service_runtime(started_service_name)
|
|
311
|
+
if exclude_local or started_service_runtime != ServiceRuntime.LOCAL:
|
|
312
|
+
# TODO: In theory, we should only be able to run the base-service when a dependent service is running if
|
|
313
|
+
# 1. the dependent service is using a mode that doesn't include the base-service
|
|
314
|
+
# 2. the base-service is using a local runtime
|
|
315
|
+
# But we don't restrict the other cases currently
|
|
316
|
+
for dependency_name in service_to_stop.config.dependencies.keys():
|
|
317
|
+
if dependency_name == started_service.config.service_name:
|
|
318
|
+
base_running_service_names.add(started_service_name)
|
|
309
319
|
|
|
310
320
|
started_service_modes = active_modes[started_service_name]
|
|
311
321
|
# Only consider the dependencies of the modes that are running
|
|
@@ -8,6 +8,8 @@ import subprocess
|
|
|
8
8
|
import xmlrpc.client
|
|
9
9
|
from enum import IntEnum
|
|
10
10
|
|
|
11
|
+
from supervisor.options import ServerOptions
|
|
12
|
+
|
|
11
13
|
from devservices.constants import DEVSERVICES_SUPERVISOR_CONFIG_DIR
|
|
12
14
|
from devservices.exceptions import SupervisorConfigError
|
|
13
15
|
from devservices.exceptions import SupervisorConnectionError
|
|
@@ -151,26 +153,36 @@ class SupervisorManager:
|
|
|
151
153
|
except xmlrpc.client.Fault as e:
|
|
152
154
|
raise SupervisorError(f"Failed to stop supervisor: {e.faultString}")
|
|
153
155
|
|
|
154
|
-
def
|
|
155
|
-
if self._is_program_running(
|
|
156
|
+
def start_process(self, name: str) -> None:
|
|
157
|
+
if self._is_program_running(name):
|
|
156
158
|
return
|
|
157
159
|
try:
|
|
158
|
-
self._get_rpc_client().supervisor.startProcess(
|
|
160
|
+
self._get_rpc_client().supervisor.startProcess(name)
|
|
159
161
|
except xmlrpc.client.Fault as e:
|
|
160
162
|
raise SupervisorProcessError(
|
|
161
|
-
f"Failed to start
|
|
163
|
+
f"Failed to start process {name}: {e.faultString}"
|
|
162
164
|
)
|
|
163
165
|
|
|
164
|
-
def
|
|
165
|
-
if not self._is_program_running(
|
|
166
|
+
def stop_process(self, name: str) -> None:
|
|
167
|
+
if not self._is_program_running(name):
|
|
166
168
|
return
|
|
167
169
|
try:
|
|
168
|
-
self._get_rpc_client().supervisor.stopProcess(
|
|
170
|
+
self._get_rpc_client().supervisor.stopProcess(name)
|
|
169
171
|
except xmlrpc.client.Fault as e:
|
|
170
172
|
raise SupervisorProcessError(
|
|
171
|
-
f"Failed to stop
|
|
173
|
+
f"Failed to stop process {name}: {e.faultString}"
|
|
172
174
|
)
|
|
173
175
|
|
|
176
|
+
def get_program_command(self, program_name: str) -> str:
|
|
177
|
+
opts = ServerOptions()
|
|
178
|
+
opts.configfile = self.config_file_path
|
|
179
|
+
opts.process_config()
|
|
180
|
+
for group in opts.process_group_configs:
|
|
181
|
+
for proc in group.process_configs:
|
|
182
|
+
if proc.name == program_name and isinstance(proc.command, str):
|
|
183
|
+
return proc.command
|
|
184
|
+
raise SupervisorConfigError(f"Program {program_name} not found in config")
|
|
185
|
+
|
|
174
186
|
def tail_program_logs(self, program_name: str) -> None:
|
|
175
187
|
if not self._is_program_running(program_name):
|
|
176
188
|
console = Console()
|
|
@@ -2,6 +2,7 @@ LICENSE.md
|
|
|
2
2
|
README.md
|
|
3
3
|
pyproject.toml
|
|
4
4
|
setup.cfg
|
|
5
|
+
devenv/sync.py
|
|
5
6
|
devservices/__init__.py
|
|
6
7
|
devservices/constants.py
|
|
7
8
|
devservices/exceptions.py
|
|
@@ -18,6 +19,7 @@ devservices/commands/list_dependencies.py
|
|
|
18
19
|
devservices/commands/list_services.py
|
|
19
20
|
devservices/commands/logs.py
|
|
20
21
|
devservices/commands/purge.py
|
|
22
|
+
devservices/commands/serve.py
|
|
21
23
|
devservices/commands/status.py
|
|
22
24
|
devservices/commands/toggle.py
|
|
23
25
|
devservices/commands/up.py
|
|
@@ -45,6 +47,7 @@ tests/commands/test_list_dependencies.py
|
|
|
45
47
|
tests/commands/test_list_services.py
|
|
46
48
|
tests/commands/test_logs.py
|
|
47
49
|
tests/commands/test_purge.py
|
|
50
|
+
tests/commands/test_serve.py
|
|
48
51
|
tests/commands/test_status.py
|
|
49
52
|
tests/commands/test_toggle.py
|
|
50
53
|
tests/commands/test_up.py
|
|
@@ -4,9 +4,9 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "devservices"
|
|
7
|
-
version = "1.1.
|
|
8
|
-
# 3.
|
|
9
|
-
requires-python = ">=3.
|
|
7
|
+
version = "1.1.2"
|
|
8
|
+
# 3.11 is just for internal pypi compat
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
10
|
dependencies = [
|
|
11
11
|
"pyyaml",
|
|
12
12
|
"sentry-devenv",
|
|
@@ -8,6 +8,7 @@ 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
|
|
11
12
|
|
|
12
13
|
TESTING_DIR = os.path.abspath(os.path.dirname(__file__))
|
|
13
14
|
|
|
@@ -26,6 +27,14 @@ def create_config_file(
|
|
|
26
27
|
yaml.dump(config, f, sort_keys=False, default_flow_style=False)
|
|
27
28
|
|
|
28
29
|
|
|
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
|
+
|
|
29
38
|
def run_git_command(command: list[str], cwd: Path) -> None:
|
|
30
39
|
subprocess.run(
|
|
31
40
|
["git", *command], cwd=cwd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|