devservices 1.0.1__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.
Files changed (56) hide show
  1. {devservices-1.0.1 → devservices-1.0.3}/PKG-INFO +1 -1
  2. {devservices-1.0.1 → devservices-1.0.3}/README.md +4 -4
  3. {devservices-1.0.1 → devservices-1.0.3}/devservices/commands/check_for_update.py +1 -1
  4. devservices-1.0.3/devservices/commands/down.py +145 -0
  5. {devservices-1.0.1 → devservices-1.0.3}/devservices/commands/list_dependencies.py +6 -1
  6. devservices-1.0.3/devservices/commands/logs.py +120 -0
  7. {devservices-1.0.1 → devservices-1.0.3}/devservices/commands/purge.py +32 -1
  8. {devservices-1.0.1 → devservices-1.0.3}/devservices/commands/status.py +62 -10
  9. devservices-1.0.3/devservices/commands/up.py +133 -0
  10. {devservices-1.0.1 → devservices-1.0.3}/devservices/commands/update.py +2 -2
  11. {devservices-1.0.1 → devservices-1.0.3}/devservices/constants.py +2 -0
  12. {devservices-1.0.1 → devservices-1.0.3}/devservices/exceptions.py +11 -0
  13. {devservices-1.0.1 → devservices-1.0.3}/devservices/main.py +24 -14
  14. {devservices-1.0.1 → devservices-1.0.3}/devservices/utils/dependencies.py +18 -7
  15. {devservices-1.0.1 → devservices-1.0.3}/devservices/utils/docker_compose.py +13 -52
  16. {devservices-1.0.1 → devservices-1.0.3}/devservices.egg-info/PKG-INFO +1 -1
  17. {devservices-1.0.1 → devservices-1.0.3}/devservices.egg-info/SOURCES.txt +5 -4
  18. {devservices-1.0.1 → devservices-1.0.3}/pyproject.toml +1 -1
  19. devservices-1.0.1/tests/commands/test_stop.py → devservices-1.0.3/tests/commands/test_down.py +98 -7
  20. devservices-1.0.3/tests/commands/test_logs.py +150 -0
  21. {devservices-1.0.1 → devservices-1.0.3}/tests/commands/test_purge.py +39 -4
  22. devservices-1.0.3/tests/commands/test_up.py +338 -0
  23. {devservices-1.0.1 → devservices-1.0.3}/tests/utils/test_dependencies.py +116 -0
  24. {devservices-1.0.1 → devservices-1.0.3}/tests/utils/test_docker_compose.py +5 -5
  25. devservices-1.0.1/devservices/commands/logs.py +0 -68
  26. devservices-1.0.1/devservices/commands/start.py +0 -70
  27. devservices-1.0.1/devservices/commands/stop.py +0 -78
  28. devservices-1.0.1/tests/commands/test_start.py +0 -183
  29. {devservices-1.0.1 → devservices-1.0.3}/LICENSE.md +0 -0
  30. {devservices-1.0.1 → devservices-1.0.3}/devservices/__init__.py +0 -0
  31. {devservices-1.0.1 → devservices-1.0.3}/devservices/commands/__init__.py +0 -0
  32. {devservices-1.0.1 → devservices-1.0.3}/devservices/commands/list_services.py +0 -0
  33. {devservices-1.0.1 → devservices-1.0.3}/devservices/configs/service_config.py +0 -0
  34. {devservices-1.0.1 → devservices-1.0.3}/devservices/utils/__init__.py +0 -0
  35. {devservices-1.0.1 → devservices-1.0.3}/devservices/utils/console.py +0 -0
  36. {devservices-1.0.1 → devservices-1.0.3}/devservices/utils/devenv.py +0 -0
  37. {devservices-1.0.1 → devservices-1.0.3}/devservices/utils/docker.py +0 -0
  38. {devservices-1.0.1 → devservices-1.0.3}/devservices/utils/file_lock.py +0 -0
  39. {devservices-1.0.1 → devservices-1.0.3}/devservices/utils/install_binary.py +0 -0
  40. {devservices-1.0.1 → devservices-1.0.3}/devservices/utils/services.py +0 -0
  41. {devservices-1.0.1 → devservices-1.0.3}/devservices/utils/state.py +0 -0
  42. {devservices-1.0.1 → devservices-1.0.3}/devservices.egg-info/dependency_links.txt +0 -0
  43. {devservices-1.0.1 → devservices-1.0.3}/devservices.egg-info/entry_points.txt +0 -0
  44. {devservices-1.0.1 → devservices-1.0.3}/devservices.egg-info/requires.txt +0 -0
  45. {devservices-1.0.1 → devservices-1.0.3}/devservices.egg-info/top_level.txt +0 -0
  46. {devservices-1.0.1 → devservices-1.0.3}/setup.cfg +0 -0
  47. {devservices-1.0.1 → devservices-1.0.3}/testing/__init__.py +0 -0
  48. {devservices-1.0.1 → devservices-1.0.3}/testing/utils.py +0 -0
  49. {devservices-1.0.1 → devservices-1.0.3}/tests/__init__.py +0 -0
  50. {devservices-1.0.1 → devservices-1.0.3}/tests/commands/test_list_services.py +0 -0
  51. {devservices-1.0.1 → devservices-1.0.3}/tests/commands/test_update.py +0 -0
  52. {devservices-1.0.1 → devservices-1.0.3}/tests/configs/test_service_config.py +0 -0
  53. {devservices-1.0.1 → devservices-1.0.3}/tests/conftest.py +0 -0
  54. {devservices-1.0.1 → devservices-1.0.3}/tests/utils/test_docker.py +0 -0
  55. {devservices-1.0.1 → devservices-1.0.3}/tests/utils/test_install_binary.py +0 -0
  56. {devservices-1.0.1 → devservices-1.0.3}/tests/utils/test_state.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: devservices
3
- Version: 1.0.1
3
+ Version: 1.0.3
4
4
  Requires-Python: >=3.10
5
5
  License-File: LICENSE.md
6
6
  Requires-Dist: pyyaml
@@ -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 starting, stopping, and managing services for development purposes.
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.1
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 start <service-name>`: Start a service and its dependencies.
27
- - `devservices stop <service-name>`: Stop a service including its dependencies.
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(current_version: str) -> str | None:
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 Exception as e:
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(args: Namespace) -> None:
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.docker_compose import run_docker_compose_command
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
- """Start a service and its dependencies."""
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 Exception as e:
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 = run_docker_compose_command(
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(args: Namespace) -> None:
44
+ def update(_args: Namespace) -> None:
45
45
  console = Console()
46
46
  current_version = metadata.version("devservices")
47
- latest_version = check_for_update(current_version)
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