devservices 1.0.4__tar.gz → 1.0.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. {devservices-1.0.4 → devservices-1.0.6}/PKG-INFO +1 -1
  2. {devservices-1.0.4 → devservices-1.0.6}/README.md +1 -1
  3. {devservices-1.0.4 → devservices-1.0.6}/devservices/commands/down.py +21 -8
  4. {devservices-1.0.4 → devservices-1.0.6}/devservices/commands/list_dependencies.py +5 -2
  5. {devservices-1.0.4 → devservices-1.0.6}/devservices/commands/list_services.py +2 -0
  6. {devservices-1.0.4 → devservices-1.0.6}/devservices/commands/logs.py +4 -1
  7. {devservices-1.0.4 → devservices-1.0.6}/devservices/commands/purge.py +10 -13
  8. {devservices-1.0.4 → devservices-1.0.6}/devservices/commands/status.py +4 -1
  9. {devservices-1.0.4 → devservices-1.0.6}/devservices/commands/up.py +9 -57
  10. {devservices-1.0.4 → devservices-1.0.6}/devservices/commands/update.py +1 -1
  11. {devservices-1.0.4 → devservices-1.0.6}/devservices/constants.py +11 -0
  12. {devservices-1.0.4 → devservices-1.0.6}/devservices/exceptions.py +8 -2
  13. {devservices-1.0.4 → devservices-1.0.6}/devservices/main.py +2 -2
  14. devservices-1.0.6/devservices/utils/check_for_update.py +59 -0
  15. {devservices-1.0.4 → devservices-1.0.6}/devservices/utils/dependencies.py +37 -10
  16. {devservices-1.0.4 → devservices-1.0.6}/devservices/utils/devenv.py +1 -1
  17. devservices-1.0.6/devservices/utils/docker.py +88 -0
  18. {devservices-1.0.4 → devservices-1.0.6}/devservices/utils/state.py +20 -11
  19. {devservices-1.0.4 → devservices-1.0.6}/devservices.egg-info/PKG-INFO +1 -1
  20. {devservices-1.0.4 → devservices-1.0.6}/devservices.egg-info/SOURCES.txt +5 -1
  21. {devservices-1.0.4 → devservices-1.0.6}/pyproject.toml +1 -1
  22. {devservices-1.0.4 → devservices-1.0.6}/tests/commands/test_down.py +35 -3
  23. devservices-1.0.6/tests/commands/test_list_dependencies.py +111 -0
  24. {devservices-1.0.4 → devservices-1.0.6}/tests/commands/test_list_services.py +4 -4
  25. {devservices-1.0.4 → devservices-1.0.6}/tests/commands/test_logs.py +33 -0
  26. {devservices-1.0.4 → devservices-1.0.6}/tests/commands/test_purge.py +100 -53
  27. devservices-1.0.6/tests/commands/test_status.py +171 -0
  28. {devservices-1.0.4 → devservices-1.0.6}/tests/commands/test_up.py +53 -58
  29. devservices-1.0.6/tests/utils/test_check_for_update.py +170 -0
  30. {devservices-1.0.4 → devservices-1.0.6}/tests/utils/test_dependencies.py +162 -15
  31. devservices-1.0.6/tests/utils/test_docker.py +182 -0
  32. devservices-1.0.6/tests/utils/test_services.py +102 -0
  33. {devservices-1.0.4 → devservices-1.0.6}/tests/utils/test_state.py +10 -10
  34. devservices-1.0.4/devservices/commands/check_for_update.py +0 -14
  35. devservices-1.0.4/devservices/utils/docker.py +0 -36
  36. devservices-1.0.4/tests/utils/test_docker.py +0 -47
  37. {devservices-1.0.4 → devservices-1.0.6}/LICENSE.md +0 -0
  38. {devservices-1.0.4 → devservices-1.0.6}/devservices/__init__.py +0 -0
  39. {devservices-1.0.4 → devservices-1.0.6}/devservices/commands/__init__.py +0 -0
  40. {devservices-1.0.4 → devservices-1.0.6}/devservices/configs/service_config.py +0 -0
  41. {devservices-1.0.4 → devservices-1.0.6}/devservices/utils/__init__.py +0 -0
  42. {devservices-1.0.4 → devservices-1.0.6}/devservices/utils/console.py +0 -0
  43. {devservices-1.0.4 → devservices-1.0.6}/devservices/utils/docker_compose.py +0 -0
  44. {devservices-1.0.4 → devservices-1.0.6}/devservices/utils/file_lock.py +0 -0
  45. {devservices-1.0.4 → devservices-1.0.6}/devservices/utils/install_binary.py +0 -0
  46. {devservices-1.0.4 → devservices-1.0.6}/devservices/utils/services.py +0 -0
  47. {devservices-1.0.4 → devservices-1.0.6}/devservices.egg-info/dependency_links.txt +0 -0
  48. {devservices-1.0.4 → devservices-1.0.6}/devservices.egg-info/entry_points.txt +0 -0
  49. {devservices-1.0.4 → devservices-1.0.6}/devservices.egg-info/requires.txt +0 -0
  50. {devservices-1.0.4 → devservices-1.0.6}/devservices.egg-info/top_level.txt +0 -0
  51. {devservices-1.0.4 → devservices-1.0.6}/setup.cfg +0 -0
  52. {devservices-1.0.4 → devservices-1.0.6}/testing/__init__.py +0 -0
  53. {devservices-1.0.4 → devservices-1.0.6}/testing/utils.py +0 -0
  54. {devservices-1.0.4 → devservices-1.0.6}/tests/__init__.py +0 -0
  55. {devservices-1.0.4 → devservices-1.0.6}/tests/commands/test_update.py +0 -0
  56. {devservices-1.0.4 → devservices-1.0.6}/tests/configs/test_service_config.py +0 -0
  57. {devservices-1.0.4 → devservices-1.0.6}/tests/conftest.py +0 -0
  58. {devservices-1.0.4 → devservices-1.0.6}/tests/utils/test_docker_compose.py +0 -0
  59. {devservices-1.0.4 → devservices-1.0.6}/tests/utils/test_install_binary.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: devservices
3
- Version: 1.0.4
3
+ Version: 1.0.6
4
4
  Requires-Python: >=3.10
5
5
  License-File: LICENSE.md
6
6
  Requires-Dist: pyyaml
@@ -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.4
14
+ devservices==1.0.6
15
15
  ```
16
16
 
17
17
 
@@ -56,10 +56,13 @@ def down(args: Namespace) -> None:
56
56
  service_name = args.service_name
57
57
  try:
58
58
  service = find_matching_service(service_name)
59
- except (ConfigError, ServiceNotFoundError) as e:
59
+ except ConfigError as e:
60
60
  capture_exception(e)
61
61
  console.failure(str(e))
62
62
  exit(1)
63
+ except ServiceNotFoundError as e:
64
+ console.failure(str(e))
65
+ exit(1)
63
66
 
64
67
  modes = service.config.modes
65
68
 
@@ -69,24 +72,34 @@ def down(args: Namespace) -> None:
69
72
  console.warning(f"{service.name} is not running")
70
73
  exit(0)
71
74
 
72
- mode = state.get_mode_for_service(service.name) or "default"
73
- mode_dependencies = modes[mode]
75
+ active_modes = state.get_active_modes_for_service(service.name)
76
+ mode_dependencies = set()
77
+ for active_mode in active_modes:
78
+ active_mode_dependencies = modes.get(active_mode, [])
79
+ mode_dependencies.update(active_mode_dependencies)
74
80
 
75
81
  with Status(
76
82
  lambda: console.warning(f"Stopping {service.name}"),
77
83
  lambda: console.success(f"{service.name} stopped"),
78
84
  ) as status:
79
85
  try:
80
- remote_dependencies = install_and_verify_dependencies(service, mode=mode)
86
+ remote_dependencies = install_and_verify_dependencies(
87
+ service, modes=active_modes
88
+ )
89
+ except DependencyError as de:
90
+ capture_exception(de)
91
+ status.failure(str(de))
92
+ exit(1)
93
+ try:
94
+ remote_dependencies = get_non_shared_remote_dependencies(
95
+ service, remote_dependencies
96
+ )
81
97
  except DependencyError as de:
82
98
  capture_exception(de)
83
99
  status.failure(str(de))
84
100
  exit(1)
85
- remote_dependencies = get_non_shared_remote_dependencies(
86
- service, remote_dependencies
87
- )
88
101
  try:
89
- _down(service, remote_dependencies, mode_dependencies, status)
102
+ _down(service, remote_dependencies, list(mode_dependencies), status)
90
103
  except DockerComposeError as dce:
91
104
  capture_exception(dce)
92
105
  status.failure(f"Failed to stop {service.name}: {dce.stderr}")
@@ -32,10 +32,13 @@ def list_dependencies(args: Namespace) -> None:
32
32
 
33
33
  try:
34
34
  service = find_matching_service(service_name)
35
- except (ConfigError, ServiceNotFoundError) as e:
35
+ except ConfigError as e:
36
36
  capture_exception(e)
37
37
  console.failure(str(e))
38
38
  exit(1)
39
+ except ServiceNotFoundError as e:
40
+ console.failure(str(e))
41
+ exit(1)
39
42
 
40
43
  dependencies = service.config.dependencies
41
44
 
@@ -45,4 +48,4 @@ def list_dependencies(args: Namespace) -> None:
45
48
 
46
49
  console.info(f"Dependencies of {service.name}:")
47
50
  for dependency_key, dependency_info in dependencies.items():
48
- console.info("-" + dependency_key + ":" + dependency_info.description)
51
+ console.info("- " + dependency_key + ": " + dependency_info.description)
@@ -47,7 +47,9 @@ def list_services(args: Namespace) -> None:
47
47
 
48
48
  for service in services_to_show:
49
49
  status = "running" if service.name in running_services else "stopped"
50
+ active_modes = state.get_active_modes_for_service(service.name)
50
51
  console.info(f"- {service.name}")
52
+ console.info(f" modes: {active_modes}")
51
53
  console.info(f" status: {status}")
52
54
  console.info(f" location: {service.repo_path}")
53
55
 
@@ -46,10 +46,13 @@ def logs(args: Namespace) -> None:
46
46
  service_name = args.service_name
47
47
  try:
48
48
  service = find_matching_service(service_name)
49
- except (ConfigError, ServiceNotFoundError) as e:
49
+ except ConfigError as e:
50
50
  capture_exception(e)
51
51
  console.failure(str(e))
52
52
  exit(1)
53
+ except ServiceNotFoundError as e:
54
+ console.failure(str(e))
55
+ exit(1)
53
56
 
54
57
  modes = service.config.modes
55
58
  # TODO: allow custom modes to be used
@@ -8,11 +8,13 @@ from argparse import ArgumentParser
8
8
  from argparse import Namespace
9
9
 
10
10
  from devservices.constants import DEVSERVICES_CACHE_DIR
11
+ from devservices.constants import DEVSERVICES_ORCHESTRATOR_LABEL
11
12
  from devservices.constants import DOCKER_NETWORK_NAME
12
13
  from devservices.exceptions import DockerDaemonNotRunningError
14
+ from devservices.exceptions import DockerError
13
15
  from devservices.utils.console import Console
14
16
  from devservices.utils.console import Status
15
- from devservices.utils.docker import stop_all_running_containers
17
+ from devservices.utils.docker import stop_matching_containers
16
18
  from devservices.utils.state import State
17
19
 
18
20
 
@@ -25,14 +27,6 @@ def purge(_args: Namespace) -> None:
25
27
  """Purge the local devservices cache."""
26
28
  console = Console()
27
29
 
28
- # Prompt the user to stop all running containers
29
- should_stop_containers = console.confirm(
30
- "Warning: Purging stops all running containers and clears devservices state. Would you like to continue?"
31
- )
32
- if not should_stop_containers:
33
- console.warning("Purge canceled")
34
- return
35
-
36
30
  if os.path.exists(DEVSERVICES_CACHE_DIR):
37
31
  try:
38
32
  shutil.rmtree(DEVSERVICES_CACHE_DIR)
@@ -42,13 +36,16 @@ def purge(_args: Namespace) -> None:
42
36
  state = State()
43
37
  state.clear_state()
44
38
  with Status(
45
- lambda: console.warning("Stopping all running containers"),
46
- lambda: console.success("All running containers have been stopped"),
39
+ lambda: console.warning("Stopping all running devservices containers"),
40
+ lambda: console.success("All running devservices containers have been stopped"),
47
41
  ):
48
42
  try:
49
- stop_all_running_containers()
43
+ stop_matching_containers(DEVSERVICES_ORCHESTRATOR_LABEL, should_remove=True)
50
44
  except DockerDaemonNotRunningError:
51
- console.warning("The docker daemon not running, no containers to stop")
45
+ console.warning("The docker daemon is not running, no containers to stop")
46
+ except DockerError as e:
47
+ console.failure(f"Failed to stop running devservices containers {e.stderr}")
48
+ exit(1)
52
49
 
53
50
  console.warning("Removing any devservices networks")
54
51
  devservices_networks = (
@@ -81,10 +81,13 @@ def status(args: Namespace) -> None:
81
81
  service_name = args.service_name
82
82
  try:
83
83
  service = find_matching_service(service_name)
84
- except (ConfigError, ServiceNotFoundError) as e:
84
+ except ConfigError as e:
85
85
  capture_exception(e)
86
86
  console.failure(str(e))
87
87
  exit(1)
88
+ except ServiceNotFoundError as e:
89
+ console.failure(str(e))
90
+ exit(1)
88
91
 
89
92
  modes = service.config.modes
90
93
  # TODO: allow custom modes to be used
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import concurrent.futures
4
3
  import os
5
4
  import subprocess
6
5
  from argparse import _SubParsersAction
@@ -23,7 +22,6 @@ from devservices.exceptions import ServiceNotFoundError
23
22
  from devservices.utils.console import Console
24
23
  from devservices.utils.console import Status
25
24
  from devservices.utils.dependencies import construct_dependency_graph
26
- from devservices.utils.dependencies import get_non_shared_remote_dependencies
27
25
  from devservices.utils.dependencies import install_and_verify_dependencies
28
26
  from devservices.utils.dependencies import InstalledRemoteDependency
29
27
  from devservices.utils.docker_compose import get_docker_compose_commands_to_run
@@ -58,64 +56,17 @@ def up(args: Namespace) -> None:
58
56
  service_name = args.service_name
59
57
  try:
60
58
  service = find_matching_service(service_name)
61
- except (ConfigError, ServiceNotFoundError) as e:
59
+ except ConfigError as e:
62
60
  capture_exception(e)
63
61
  console.failure(str(e))
64
62
  exit(1)
63
+ except ServiceNotFoundError as e:
64
+ console.failure(str(e))
65
+ exit(1)
65
66
 
66
67
  modes = service.config.modes
67
68
  mode = args.mode
68
69
 
69
- state = State()
70
- started_services = state.get_started_services()
71
- running_mode = state.get_mode_for_service(service.name) or "default"
72
-
73
- # TODO: Remove this once we properly handle mode switching
74
- if service.name in started_services and running_mode != mode:
75
- console.warning(
76
- f"Service '{service.name}' is already running in mode: '{running_mode}', restarting in mode: '{mode}'"
77
- )
78
- with Status() as status:
79
- try:
80
- remote_dependencies = install_and_verify_dependencies(
81
- service, mode=running_mode
82
- )
83
- except DependencyError as de:
84
- capture_exception(de)
85
- status.failure(str(de))
86
- exit(1)
87
- except ModeDoesNotExistError as mde:
88
- capture_exception(mde)
89
- status.failure(str(mde))
90
- exit(1)
91
- service_config_file_path = os.path.join(
92
- service.repo_path, DEVSERVICES_DIR_NAME, CONFIG_FILE_NAME
93
- )
94
- current_env = os.environ.copy()
95
- running_mode_dependencies = modes[running_mode]
96
- remote_dependencies_to_bring_down = get_non_shared_remote_dependencies(
97
- service, remote_dependencies
98
- )
99
- down_docker_compose_commands = get_docker_compose_commands_to_run(
100
- service=service,
101
- remote_dependencies=list(remote_dependencies_to_bring_down),
102
- current_env=current_env,
103
- command="down",
104
- options=[],
105
- service_config_file_path=service_config_file_path,
106
- mode_dependencies=running_mode_dependencies,
107
- )
108
-
109
- with concurrent.futures.ThreadPoolExecutor() as executor:
110
- futures = [
111
- executor.submit(run_cmd, cmd, current_env)
112
- for cmd in down_docker_compose_commands
113
- ]
114
- for future in concurrent.futures.as_completed(futures):
115
- future.result()
116
-
117
- state.remove_started_service(service.name)
118
-
119
70
  with Status(
120
71
  lambda: console.warning(f"Starting '{service.name}' in mode: '{mode}'"),
121
72
  lambda: console.success(f"{service.name} started"),
@@ -123,7 +74,7 @@ def up(args: Namespace) -> None:
123
74
  try:
124
75
  status.info("Retrieving dependencies")
125
76
  remote_dependencies = install_and_verify_dependencies(
126
- service, force_update_dependencies=True, mode=mode
77
+ service, force_update_dependencies=True, modes=[mode]
127
78
  )
128
79
  except DependencyError as de:
129
80
  capture_exception(de)
@@ -139,14 +90,14 @@ def up(args: Namespace) -> None:
139
90
  pass
140
91
  try:
141
92
  mode_dependencies = modes[mode]
142
- _up(service, remote_dependencies, mode_dependencies, status)
93
+ _up(service, [mode], remote_dependencies, mode_dependencies, status)
143
94
  except DockerComposeError as dce:
144
95
  capture_exception(dce)
145
96
  status.failure(f"Failed to start {service.name}: {dce.stderr}")
146
97
  exit(1)
147
98
  # TODO: We should factor in healthchecks here before marking service as running
148
99
  state = State()
149
- state.add_started_service(service.name, mode)
100
+ state.update_started_service(service.name, mode)
150
101
 
151
102
 
152
103
  def _bring_up_dependency(
@@ -160,6 +111,7 @@ def _bring_up_dependency(
160
111
 
161
112
  def _up(
162
113
  service: Service,
114
+ modes: list[str],
163
115
  remote_dependencies: set[InstalledRemoteDependency],
164
116
  mode_dependencies: list[str],
165
117
  status: Status,
@@ -177,7 +129,7 @@ def _up(
177
129
  DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
178
130
  ] = relative_local_dependency_directory
179
131
  options = ["-d"]
180
- dependency_graph = construct_dependency_graph(service)
132
+ dependency_graph = construct_dependency_graph(service, modes=modes)
181
133
  starting_order = dependency_graph.get_starting_order()
182
134
  sorted_remote_dependencies = sorted(
183
135
  remote_dependencies, key=lambda dep: starting_order.index(dep.service_name)
@@ -7,10 +7,10 @@ from argparse import ArgumentParser
7
7
  from argparse import Namespace
8
8
  from importlib import metadata
9
9
 
10
- from devservices.commands.check_for_update import check_for_update
11
10
  from devservices.constants import DEVSERVICES_DOWNLOAD_URL
12
11
  from devservices.exceptions import BinaryInstallError
13
12
  from devservices.exceptions import DevservicesUpdateError
13
+ from devservices.utils.check_for_update import check_for_update
14
14
  from devservices.utils.console import Console
15
15
  from devservices.utils.install_binary import install_binary
16
16
 
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
+ from datetime import timedelta
4
5
 
5
6
  MINIMUM_DOCKER_COMPOSE_VERSION = "2.29.7"
6
7
  DEVSERVICES_DIR_NAME = "devservices"
@@ -12,6 +13,7 @@ DEVSERVICES_LOCAL_DIR = os.path.expanduser("~/.local/share/sentry-devservices")
12
13
  DEVSERVICES_DEPENDENCIES_CACHE_DIR = os.path.join(DEVSERVICES_CACHE_DIR, "dependencies")
13
14
  DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY = "DEVSERVICES_DEPENDENCIES_CACHE_DIR"
14
15
  STATE_DB_FILE = os.path.join(DEVSERVICES_LOCAL_DIR, "state")
16
+ DEVSERVICES_ORCHESTRATOR_LABEL = "orchestrator=devservices"
15
17
  DOCKER_COMPOSE_COMMAND_LENGTH = 7
16
18
 
17
19
  DEPENDENCY_CONFIG_VERSION = "v1"
@@ -21,9 +23,18 @@ DEPENDENCY_GIT_PARTIAL_CLONE_CONFIG_OPTIONS = {
21
23
  "core.sparseCheckout": "true",
22
24
  }
23
25
 
26
+ DEVSERVICES_RELEASES_URL = (
27
+ "https://api.github.com/repos/getsentry/devservices/releases/latest"
28
+ )
24
29
  DOCKER_COMPOSE_DOWNLOAD_URL = "https://github.com/docker/compose/releases/download"
25
30
  DEVSERVICES_DOWNLOAD_URL = "https://github.com/getsentry/devservices/releases/download"
26
31
  BINARY_PERMISSIONS = 0o755
27
32
  MAX_LOG_LINES = "100"
28
33
  LOGGER_NAME = "devservices"
29
34
  DOCKER_NETWORK_NAME = "devservices"
35
+
36
+ # Latest Version Cache
37
+ DEVSERVICES_LATEST_VERSION_CACHE_FILE = os.path.join(
38
+ DEVSERVICES_CACHE_DIR, "latest_version.txt"
39
+ )
40
+ DEVSERVICES_LATEST_VERSION_CACHE_TTL = timedelta(minutes=15)
@@ -57,8 +57,8 @@ class DockerComposeInstallationError(BinaryInstallError):
57
57
  pass
58
58
 
59
59
 
60
- class DockerComposeError(Exception):
61
- """Base class for Docker Compose related errors."""
60
+ class DockerError(Exception):
61
+ """Base class for Docker related errors."""
62
62
 
63
63
  def __init__(self, command: str, returncode: int, stdout: str, stderr: str):
64
64
  self.command = command
@@ -67,6 +67,12 @@ class DockerComposeError(Exception):
67
67
  self.stderr = stderr
68
68
 
69
69
 
70
+ class DockerComposeError(DockerError):
71
+ """Base class for Docker Compose related errors."""
72
+
73
+ pass
74
+
75
+
70
76
  class ModeDoesNotExistError(Exception):
71
77
  """Raised when a mode does not exist."""
72
78
 
@@ -23,10 +23,10 @@ from devservices.commands import purge
23
23
  from devservices.commands import status
24
24
  from devservices.commands import up
25
25
  from devservices.commands import update
26
- from devservices.commands.check_for_update import check_for_update
27
26
  from devservices.constants import LOGGER_NAME
28
27
  from devservices.exceptions import DockerComposeInstallationError
29
28
  from devservices.exceptions import DockerDaemonNotRunningError
29
+ from devservices.utils.check_for_update import check_for_update
30
30
  from devservices.utils.console import Console
31
31
  from devservices.utils.docker_compose import check_docker_compose_version
32
32
 
@@ -102,7 +102,7 @@ def main() -> None:
102
102
  else:
103
103
  parser.print_help()
104
104
 
105
- if args.command != "update":
105
+ if args.command != "update" and os.environ.get("CI") != "true":
106
106
  newest_version = check_for_update()
107
107
  if newest_version != current_version:
108
108
  console.warning(
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from datetime import datetime
6
+ from datetime import timedelta
7
+ from urllib.request import urlopen
8
+
9
+ from devservices.constants import DEVSERVICES_CACHE_DIR
10
+ from devservices.constants import DEVSERVICES_LATEST_VERSION_CACHE_FILE
11
+ from devservices.constants import DEVSERVICES_LATEST_VERSION_CACHE_TTL
12
+ from devservices.constants import DEVSERVICES_RELEASES_URL
13
+
14
+
15
+ def _delete_cached_version() -> None:
16
+ if os.path.exists(DEVSERVICES_LATEST_VERSION_CACHE_FILE):
17
+ os.remove(DEVSERVICES_LATEST_VERSION_CACHE_FILE)
18
+
19
+
20
+ def _get_cache_age() -> timedelta:
21
+ if os.path.exists(DEVSERVICES_LATEST_VERSION_CACHE_FILE):
22
+ file_modification_time = datetime.fromtimestamp(
23
+ os.path.getmtime(DEVSERVICES_LATEST_VERSION_CACHE_FILE)
24
+ )
25
+ return datetime.now() - file_modification_time
26
+ return timedelta.max
27
+
28
+
29
+ def _get_cached_version() -> str | None:
30
+ cache_age = _get_cache_age()
31
+ if cache_age < DEVSERVICES_LATEST_VERSION_CACHE_TTL:
32
+ with open(DEVSERVICES_LATEST_VERSION_CACHE_FILE, "r", encoding="utf-8") as f:
33
+ return f.read()
34
+ else:
35
+ _delete_cached_version()
36
+ return None
37
+
38
+
39
+ def _set_cached_version(latest_version: str) -> None:
40
+ with open(DEVSERVICES_LATEST_VERSION_CACHE_FILE, "w", encoding="utf-8") as f:
41
+ f.write(latest_version)
42
+
43
+
44
+ def check_for_update() -> str | None:
45
+ os.makedirs(DEVSERVICES_CACHE_DIR, exist_ok=True)
46
+
47
+ cached_version = _get_cached_version()
48
+ if cached_version is not None:
49
+ return cached_version
50
+
51
+ with urlopen(DEVSERVICES_RELEASES_URL) as response:
52
+ if response.status == 200:
53
+ data = json.loads(response.read())
54
+ latest_version = str(data["tag_name"])
55
+
56
+ _set_cached_version(latest_version)
57
+
58
+ return latest_version
59
+ return None
@@ -160,11 +160,20 @@ class GitConfigManager:
160
160
 
161
161
 
162
162
  def install_and_verify_dependencies(
163
- service: Service, force_update_dependencies: bool = False, mode: str = "default"
163
+ service: Service,
164
+ force_update_dependencies: bool = False,
165
+ modes: list[str] | None = None,
164
166
  ) -> set[InstalledRemoteDependency]:
165
- if mode not in service.config.modes:
166
- raise ModeDoesNotExistError(service_name=service.name, mode=mode)
167
- mode_dependencies = set(service.config.modes[mode])
167
+ """
168
+ Install and verify dependencies for a service
169
+ """
170
+ if modes is None:
171
+ modes = ["default"]
172
+ mode_dependencies = set()
173
+ for mode in modes:
174
+ if mode not in service.config.modes:
175
+ raise ModeDoesNotExistError(service_name=service.name, mode=mode)
176
+ mode_dependencies.update(service.config.modes[mode])
168
177
  matching_dependencies = [
169
178
  dependency
170
179
  for dependency_key, dependency in list(service.config.dependencies.items())
@@ -346,8 +355,18 @@ def install_dependency(dependency: RemoteConfig) -> set[InstalledRemoteDependenc
346
355
  branch=dependency.branch,
347
356
  ) from e
348
357
 
349
- nested_dependencies = list(installed_config.dependencies.values())
350
- nested_remote_configs = _get_remote_configs(nested_dependencies)
358
+ if dependency.mode not in installed_config.modes:
359
+ raise ModeDoesNotExistError(
360
+ service_name=installed_config.service_name,
361
+ mode=dependency.mode,
362
+ )
363
+
364
+ active_nested_dependencies = [
365
+ nested_dependency
366
+ for nested_dependency_name, nested_dependency in installed_config.dependencies.items()
367
+ if nested_dependency_name in installed_config.modes[dependency.mode]
368
+ ]
369
+ nested_remote_configs = _get_remote_configs(active_nested_dependencies)
351
370
 
352
371
  installed_dependencies: set[InstalledRemoteDependency] = set(
353
372
  [
@@ -526,15 +545,23 @@ def get_remote_dependency_config(remote_config: RemoteConfig) -> ServiceConfig:
526
545
  return load_service_config_from_file(dependency_repo_dir)
527
546
 
528
547
 
529
- def construct_dependency_graph(service: Service) -> DependencyGraph:
548
+ def construct_dependency_graph(service: Service, modes: list[str]) -> DependencyGraph:
530
549
  dependency_graph = DependencyGraph()
531
550
 
532
- def _construct_dependency_graph(service_config: ServiceConfig) -> None:
551
+ def _construct_dependency_graph(
552
+ service_config: ServiceConfig, modes: list[str]
553
+ ) -> None:
554
+ service_mode_dependencies = set()
555
+ for mode in modes:
556
+ service_mode_dependencies.update(service_config.modes.get(mode, []))
533
557
  for dependency_name, dependency in service_config.dependencies.items():
558
+ # Skip the dependency if it's not in the modes (since it may not be installed and we don't care about it)
559
+ if dependency_name not in service_mode_dependencies:
560
+ continue
534
561
  dependency_graph.add_edge(service_config.service_name, dependency_name)
535
562
  if _has_remote_config(dependency.remote):
536
563
  dependency_config = get_remote_dependency_config(dependency.remote)
537
- _construct_dependency_graph(dependency_config)
564
+ _construct_dependency_graph(dependency_config, [dependency.remote.mode])
538
565
 
539
- _construct_dependency_graph(service.config)
566
+ _construct_dependency_graph(service.config, modes)
540
567
  return dependency_graph
@@ -13,7 +13,7 @@ def get_coderoot() -> str:
13
13
  config_path = os.path.join(home, ".config", "sentry-devenv", "config.ini")
14
14
  try:
15
15
  devenv_config: ConfigParser = read_config(config_path)
16
- return devenv_config.get("devenv", "coderoot", fallback="")
16
+ return os.path.expanduser(devenv_config.get("devenv", "coderoot", fallback=""))
17
17
  except (FileNotFoundError, NoSectionError, NoOptionError):
18
18
  # TODO: Handle the case where there is no config file or the coderoot is not set
19
19
  raise Exception("Failed to read code root from config")
@@ -0,0 +1,88 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+
5
+ from devservices.exceptions import DockerDaemonNotRunningError
6
+ from devservices.exceptions import DockerError
7
+
8
+
9
+ def check_docker_daemon_running() -> None:
10
+ """Checks if the Docker daemon is running. Raises DockerDaemonNotRunningError if not."""
11
+ try:
12
+ subprocess.run(
13
+ ["docker", "info"],
14
+ capture_output=True,
15
+ text=True,
16
+ check=True,
17
+ )
18
+ except subprocess.CalledProcessError as e:
19
+ raise DockerDaemonNotRunningError from e
20
+
21
+
22
+ def get_matching_containers(label: str) -> list[str]:
23
+ """
24
+ Returns a list of container IDs with the given label
25
+ """
26
+ check_docker_daemon_running()
27
+ try:
28
+ return (
29
+ subprocess.check_output(
30
+ [
31
+ "docker",
32
+ "ps",
33
+ "-q",
34
+ "--filter",
35
+ f"label={label}",
36
+ ],
37
+ stderr=subprocess.DEVNULL,
38
+ )
39
+ .decode()
40
+ .strip()
41
+ .splitlines()
42
+ )
43
+ except subprocess.CalledProcessError as e:
44
+ raise DockerError(
45
+ command=f"docker ps -q --filter label={label}",
46
+ returncode=e.returncode,
47
+ stdout=e.stdout,
48
+ stderr=e.stderr,
49
+ ) from e
50
+
51
+
52
+ def stop_matching_containers(label: str, should_remove: bool = False) -> None:
53
+ """
54
+ Stops all containers with the given label.
55
+ If should_remove is True, the containers will be removed.
56
+ """
57
+ matching_containers = get_matching_containers(label)
58
+ if len(matching_containers) == 0:
59
+ return
60
+ try:
61
+ subprocess.run(
62
+ ["docker", "stop"] + matching_containers,
63
+ check=True,
64
+ stdout=subprocess.DEVNULL,
65
+ stderr=subprocess.DEVNULL,
66
+ )
67
+ except subprocess.CalledProcessError as e:
68
+ raise DockerError(
69
+ command=f"docker stop {' '.join(matching_containers)}",
70
+ returncode=e.returncode,
71
+ stdout=e.stdout,
72
+ stderr=e.stderr,
73
+ ) from e
74
+ if should_remove:
75
+ try:
76
+ subprocess.run(
77
+ ["docker", "rm"] + matching_containers,
78
+ check=True,
79
+ stdout=subprocess.DEVNULL,
80
+ stderr=subprocess.DEVNULL,
81
+ )
82
+ except subprocess.CalledProcessError as e:
83
+ raise DockerError(
84
+ command=f"docker rm {' '.join(matching_containers)}",
85
+ returncode=e.returncode,
86
+ stdout=e.stdout,
87
+ stderr=e.stderr,
88
+ ) from e