devservices 0.0.4__tar.gz → 0.0.5__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-0.0.4 → devservices-0.0.5}/PKG-INFO +1 -1
- {devservices-0.0.4 → devservices-0.0.5}/README.md +2 -2
- {devservices-0.0.4 → devservices-0.0.5}/devservices/commands/list_services.py +5 -4
- {devservices-0.0.4 → devservices-0.0.5}/devservices/commands/logs.py +16 -2
- devservices-0.0.5/devservices/commands/purge.py +25 -0
- {devservices-0.0.4 → devservices-0.0.5}/devservices/commands/start.py +13 -1
- {devservices-0.0.4 → devservices-0.0.5}/devservices/commands/status.py +14 -4
- {devservices-0.0.4 → devservices-0.0.5}/devservices/commands/stop.py +14 -0
- {devservices-0.0.4 → devservices-0.0.5}/devservices/constants.py +2 -0
- {devservices-0.0.4 → devservices-0.0.5}/devservices/exceptions.py +20 -2
- {devservices-0.0.4 → devservices-0.0.5}/devservices/main.py +12 -1
- {devservices-0.0.4 → devservices-0.0.5}/devservices/utils/dependencies.py +24 -1
- devservices-0.0.5/devservices/utils/docker.py +19 -0
- {devservices-0.0.4 → devservices-0.0.5}/devservices/utils/docker_compose.py +15 -21
- devservices-0.0.5/devservices/utils/state.py +81 -0
- {devservices-0.0.4 → devservices-0.0.5}/devservices.egg-info/PKG-INFO +1 -1
- {devservices-0.0.4 → devservices-0.0.5}/devservices.egg-info/SOURCES.txt +6 -1
- {devservices-0.0.4 → devservices-0.0.5}/pyproject.toml +1 -1
- {devservices-0.0.4 → devservices-0.0.5}/tests/commands/test_list_services.py +9 -14
- devservices-0.0.5/tests/commands/test_purge.py +35 -0
- {devservices-0.0.4 → devservices-0.0.5}/tests/commands/test_start.py +13 -2
- {devservices-0.0.4 → devservices-0.0.5}/tests/commands/test_stop.py +25 -5
- devservices-0.0.5/tests/conftest.py +10 -0
- {devservices-0.0.4 → devservices-0.0.5}/tests/utils/test_dependencies.py +227 -0
- {devservices-0.0.4 → devservices-0.0.5}/tests/utils/test_docker_compose.py +87 -24
- devservices-0.0.5/tests/utils/test_state.py +54 -0
- devservices-0.0.4/tests/conftest.py +0 -0
- {devservices-0.0.4 → devservices-0.0.5}/LICENSE.md +0 -0
- {devservices-0.0.4 → devservices-0.0.5}/devservices/__init__.py +0 -0
- {devservices-0.0.4 → devservices-0.0.5}/devservices/commands/__init__.py +0 -0
- {devservices-0.0.4 → devservices-0.0.5}/devservices/commands/check_for_update.py +0 -0
- {devservices-0.0.4 → devservices-0.0.5}/devservices/commands/list_dependencies.py +0 -0
- {devservices-0.0.4 → devservices-0.0.5}/devservices/commands/update.py +0 -0
- {devservices-0.0.4 → devservices-0.0.5}/devservices/configs/service_config.py +0 -0
- {devservices-0.0.4 → devservices-0.0.5}/devservices/utils/__init__.py +0 -0
- {devservices-0.0.4 → devservices-0.0.5}/devservices/utils/console.py +0 -0
- {devservices-0.0.4 → devservices-0.0.5}/devservices/utils/devenv.py +0 -0
- {devservices-0.0.4 → devservices-0.0.5}/devservices/utils/file_lock.py +0 -0
- {devservices-0.0.4 → devservices-0.0.5}/devservices/utils/install_binary.py +0 -0
- {devservices-0.0.4 → devservices-0.0.5}/devservices/utils/services.py +0 -0
- {devservices-0.0.4 → devservices-0.0.5}/devservices.egg-info/dependency_links.txt +0 -0
- {devservices-0.0.4 → devservices-0.0.5}/devservices.egg-info/entry_points.txt +0 -0
- {devservices-0.0.4 → devservices-0.0.5}/devservices.egg-info/requires.txt +0 -0
- {devservices-0.0.4 → devservices-0.0.5}/devservices.egg-info/top_level.txt +0 -0
- {devservices-0.0.4 → devservices-0.0.5}/setup.cfg +0 -0
- {devservices-0.0.4 → devservices-0.0.5}/testing/__init__.py +0 -0
- {devservices-0.0.4 → devservices-0.0.5}/testing/utils.py +0 -0
- {devservices-0.0.4 → devservices-0.0.5}/tests/__init__.py +0 -0
- {devservices-0.0.4 → devservices-0.0.5}/tests/commands/test_update.py +0 -0
- {devservices-0.0.4 → devservices-0.0.5}/tests/configs/test_service_config.py +0 -0
- {devservices-0.0.4 → devservices-0.0.5}/tests/utils/test_install_binary.py +0 -0
|
@@ -13,14 +13,14 @@ A system-wide installation can be done by downloading the binary of the latest r
|
|
|
13
13
|
```
|
|
14
14
|
PLATFORM=darwin # Options: darwin/linux
|
|
15
15
|
INSTALL_DIR="$HOME/.local/bin"
|
|
16
|
-
curl -L "https://github.com/getsentry/devservices/releases/download/0.0.
|
|
16
|
+
curl -L "https://github.com/getsentry/devservices/releases/download/0.0.5/devservices-$PLATFORM" -o "$INSTALL_DIR/devservices"
|
|
17
17
|
chmod +x "$INSTALL_DIR/devservices"
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
Alternatively, if the repository you're working in has a python virtualenv, you can simply add this to the requirements-dev.txt:
|
|
21
21
|
|
|
22
22
|
```
|
|
23
|
-
devservices==0.0.
|
|
23
|
+
devservices==0.0.5
|
|
24
24
|
```
|
|
25
25
|
## Usage
|
|
26
26
|
|
|
@@ -5,8 +5,8 @@ from argparse import ArgumentParser
|
|
|
5
5
|
from argparse import Namespace
|
|
6
6
|
|
|
7
7
|
from devservices.utils.devenv import get_coderoot
|
|
8
|
-
from devservices.utils.docker_compose import get_active_docker_compose_projects
|
|
9
8
|
from devservices.utils.services import get_local_services
|
|
9
|
+
from devservices.utils.state import State
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
@@ -28,14 +28,15 @@ def list_services(args: Namespace) -> None:
|
|
|
28
28
|
# Get all of the services installed locally
|
|
29
29
|
coderoot = get_coderoot()
|
|
30
30
|
services = get_local_services(coderoot)
|
|
31
|
-
|
|
31
|
+
state = State()
|
|
32
|
+
running_services = state.get_started_services()
|
|
32
33
|
|
|
33
34
|
if not services:
|
|
34
35
|
print("No services found")
|
|
35
36
|
return
|
|
36
37
|
|
|
37
38
|
services_to_show = (
|
|
38
|
-
services if args.all else [s for s in services if s.name in
|
|
39
|
+
services if args.all else [s for s in services if s.name in running_services]
|
|
39
40
|
)
|
|
40
41
|
|
|
41
42
|
if args.all:
|
|
@@ -44,7 +45,7 @@ def list_services(args: Namespace) -> None:
|
|
|
44
45
|
print("Running services:")
|
|
45
46
|
|
|
46
47
|
for service in services_to_show:
|
|
47
|
-
status = "running" if service.name in
|
|
48
|
+
status = "running" if service.name in running_services else "stopped"
|
|
48
49
|
print(f"- {service.name}")
|
|
49
50
|
print(f" status: {status}")
|
|
50
51
|
print(f" location: {service.repo_path}")
|
|
@@ -5,9 +5,12 @@ from argparse import _SubParsersAction
|
|
|
5
5
|
from argparse import ArgumentParser
|
|
6
6
|
from argparse import Namespace
|
|
7
7
|
|
|
8
|
+
from devservices.constants import MAX_LOG_LINES
|
|
9
|
+
from devservices.exceptions import DependencyError
|
|
8
10
|
from devservices.exceptions import DockerComposeError
|
|
9
11
|
from devservices.utils.docker_compose import run_docker_compose_command
|
|
10
12
|
from devservices.utils.services import find_matching_service
|
|
13
|
+
from devservices.utils.state import State
|
|
11
14
|
|
|
12
15
|
|
|
13
16
|
def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
@@ -35,11 +38,22 @@ def logs(args: Namespace) -> None:
|
|
|
35
38
|
mode_to_use = "default"
|
|
36
39
|
mode_dependencies = modes[mode_to_use]
|
|
37
40
|
|
|
41
|
+
state = State()
|
|
42
|
+
running_services = state.get_started_services()
|
|
43
|
+
if service_name not in running_services:
|
|
44
|
+
print(f"Service {service_name} is not running")
|
|
45
|
+
return
|
|
46
|
+
|
|
38
47
|
try:
|
|
39
|
-
|
|
48
|
+
logs_output = run_docker_compose_command(
|
|
49
|
+
service, "logs", mode_dependencies, options=["-n", MAX_LOG_LINES]
|
|
50
|
+
)
|
|
51
|
+
except DependencyError as de:
|
|
52
|
+
print(str(de))
|
|
53
|
+
exit(1)
|
|
40
54
|
except DockerComposeError as dce:
|
|
41
55
|
print(f"Failed to get logs for {service.name}: {dce.stderr}")
|
|
42
56
|
exit(1)
|
|
43
|
-
for log in
|
|
57
|
+
for log in logs_output:
|
|
44
58
|
sys.stdout.write(log.stdout)
|
|
45
59
|
sys.stdout.flush()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
from argparse import _SubParsersAction
|
|
6
|
+
from argparse import ArgumentParser
|
|
7
|
+
from argparse import Namespace
|
|
8
|
+
|
|
9
|
+
from devservices.constants import DEVSERVICES_CACHE_DIR
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
13
|
+
parser = subparsers.add_parser("purge", help="Purge the local devservices cache")
|
|
14
|
+
parser.set_defaults(func=purge)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def purge(args: Namespace) -> None:
|
|
18
|
+
"""Purge the local devservices cache."""
|
|
19
|
+
if os.path.exists(DEVSERVICES_CACHE_DIR):
|
|
20
|
+
try:
|
|
21
|
+
shutil.rmtree(DEVSERVICES_CACHE_DIR)
|
|
22
|
+
except PermissionError as e:
|
|
23
|
+
print(f"Failed to purge cache: {e}")
|
|
24
|
+
exit(1)
|
|
25
|
+
print("The local devservices cache has been purged")
|
|
@@ -4,10 +4,12 @@ from argparse import _SubParsersAction
|
|
|
4
4
|
from argparse import ArgumentParser
|
|
5
5
|
from argparse import Namespace
|
|
6
6
|
|
|
7
|
+
from devservices.exceptions import DependencyError
|
|
7
8
|
from devservices.exceptions import DockerComposeError
|
|
8
9
|
from devservices.utils.console import Status
|
|
9
10
|
from devservices.utils.docker_compose import run_docker_compose_command
|
|
10
11
|
from devservices.utils.services import find_matching_service
|
|
12
|
+
from devservices.utils.state import State
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
@@ -35,8 +37,18 @@ def start(args: Namespace) -> None:
|
|
|
35
37
|
with Status(f"Starting {service.name}", f"{service.name} started") as status:
|
|
36
38
|
try:
|
|
37
39
|
run_docker_compose_command(
|
|
38
|
-
service,
|
|
40
|
+
service,
|
|
41
|
+
"up",
|
|
42
|
+
mode_dependencies,
|
|
43
|
+
["-d"],
|
|
44
|
+
force_update_dependencies=True,
|
|
39
45
|
)
|
|
46
|
+
except DependencyError as de:
|
|
47
|
+
status.print(str(de))
|
|
48
|
+
exit(1)
|
|
40
49
|
except DockerComposeError as dce:
|
|
41
50
|
status.print(f"Failed to start {service.name}: {dce.stderr}")
|
|
42
51
|
exit(1)
|
|
52
|
+
# TODO: We should factor in healthchecks here before marking service as running
|
|
53
|
+
state = State()
|
|
54
|
+
state.add_started_service(service.name, mode_to_start)
|
|
@@ -6,6 +6,7 @@ from argparse import _SubParsersAction
|
|
|
6
6
|
from argparse import ArgumentParser
|
|
7
7
|
from argparse import Namespace
|
|
8
8
|
|
|
9
|
+
from devservices.exceptions import DependencyError
|
|
9
10
|
from devservices.exceptions import DockerComposeError
|
|
10
11
|
from devservices.utils.docker_compose import run_docker_compose_command
|
|
11
12
|
from devservices.utils.services import find_matching_service
|
|
@@ -33,11 +34,13 @@ def format_status_output(status_json: str) -> str:
|
|
|
33
34
|
service = json.loads(service_status)
|
|
34
35
|
name = service["Service"]
|
|
35
36
|
state = service["State"]
|
|
37
|
+
container_name = service["Name"]
|
|
36
38
|
health = service.get("Health", "N/A")
|
|
37
39
|
ports = service.get("Publishers", [])
|
|
38
40
|
running_for = service.get("RunningFor", "N/A")
|
|
39
41
|
|
|
40
42
|
output.append(f"{name}")
|
|
43
|
+
output.append(f"Container: {container_name}")
|
|
41
44
|
output.append(f"Status: {state}")
|
|
42
45
|
output.append(f"Health: {health}")
|
|
43
46
|
output.append(f"Uptime: {running_for}")
|
|
@@ -71,18 +74,25 @@ def status(args: Namespace) -> None:
|
|
|
71
74
|
mode_dependencies = modes[mode_to_view]
|
|
72
75
|
|
|
73
76
|
try:
|
|
74
|
-
|
|
77
|
+
status_json_results = run_docker_compose_command(
|
|
75
78
|
service, "ps", mode_dependencies, options=["--format", "json"]
|
|
76
79
|
)
|
|
80
|
+
except DependencyError as de:
|
|
81
|
+
print(str(de))
|
|
82
|
+
exit(1)
|
|
77
83
|
except DockerComposeError as dce:
|
|
78
84
|
print(f"Failed to get status for {service.name}: {dce.stderr}")
|
|
79
85
|
exit(1)
|
|
80
|
-
|
|
81
|
-
if
|
|
86
|
+
|
|
87
|
+
# Filter out empty stdout to help us determine if the service is running
|
|
88
|
+
status_json_results = [
|
|
89
|
+
status_json for status_json in status_json_results if status_json.stdout
|
|
90
|
+
]
|
|
91
|
+
if len(status_json_results) == 0:
|
|
82
92
|
print(f"{service.name} is not running")
|
|
83
93
|
return
|
|
84
94
|
output = f"Service: {service.name}\n\n"
|
|
85
|
-
for status_json in
|
|
95
|
+
for status_json in status_json_results:
|
|
86
96
|
output += format_status_output(status_json.stdout)
|
|
87
97
|
output += "=" * LINE_LENGTH
|
|
88
98
|
sys.stdout.write(output + "\n")
|
|
@@ -4,10 +4,12 @@ from argparse import _SubParsersAction
|
|
|
4
4
|
from argparse import ArgumentParser
|
|
5
5
|
from argparse import Namespace
|
|
6
6
|
|
|
7
|
+
from devservices.exceptions import DependencyError
|
|
7
8
|
from devservices.exceptions import DockerComposeError
|
|
8
9
|
from devservices.utils.console import Status
|
|
9
10
|
from devservices.utils.docker_compose import run_docker_compose_command
|
|
10
11
|
from devservices.utils.services import find_matching_service
|
|
12
|
+
from devservices.utils.state import State
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
@@ -31,10 +33,22 @@ def stop(args: Namespace) -> None:
|
|
|
31
33
|
# TODO: allow custom modes to be used
|
|
32
34
|
mode_to_stop = "default"
|
|
33
35
|
mode_dependencies = modes[mode_to_stop]
|
|
36
|
+
state = State()
|
|
37
|
+
started_services = state.get_started_services()
|
|
38
|
+
if service.name not in started_services:
|
|
39
|
+
print(f"{service.name} is not running")
|
|
40
|
+
exit(0)
|
|
34
41
|
|
|
35
42
|
with Status(f"Stopping {service.name}", f"{service.name} stopped") as status:
|
|
36
43
|
try:
|
|
37
44
|
run_docker_compose_command(service, "down", mode_dependencies)
|
|
45
|
+
except DependencyError as de:
|
|
46
|
+
status.print(str(de))
|
|
47
|
+
exit(1)
|
|
38
48
|
except DockerComposeError as dce:
|
|
39
49
|
status.print(f"Failed to stop {service.name}: {dce.stderr}")
|
|
40
50
|
exit(1)
|
|
51
|
+
|
|
52
|
+
# TODO: We should factor in healthchecks here before marking service as stopped
|
|
53
|
+
state = State()
|
|
54
|
+
state.remove_started_service(service.name)
|
|
@@ -11,6 +11,7 @@ DEVSERVICES_CACHE_DIR = os.path.expanduser("~/.cache/sentry-devservices")
|
|
|
11
11
|
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
|
+
STATE_DB_FILE = os.path.join(DEVSERVICES_LOCAL_DIR, "state")
|
|
14
15
|
|
|
15
16
|
DEPENDENCY_CONFIG_VERSION = "v1"
|
|
16
17
|
DEPENDENCY_GIT_PARTIAL_CLONE_CONFIG_OPTIONS = {
|
|
@@ -22,3 +23,4 @@ DEPENDENCY_GIT_PARTIAL_CLONE_CONFIG_OPTIONS = {
|
|
|
22
23
|
DOCKER_COMPOSE_DOWNLOAD_URL = "https://github.com/docker/compose/releases/download"
|
|
23
24
|
DEVSERVICES_DOWNLOAD_URL = "https://github.com/getsentry/devservices/releases/download"
|
|
24
25
|
BINARY_PERMISSIONS = 0o755
|
|
26
|
+
MAX_LOG_LINES = "100"
|
|
@@ -43,6 +43,12 @@ class DevservicesUpdateError(BinaryInstallError):
|
|
|
43
43
|
pass
|
|
44
44
|
|
|
45
45
|
|
|
46
|
+
class DockerDaemonNotRunningError(Exception):
|
|
47
|
+
"""Raised when the Docker daemon is not running."""
|
|
48
|
+
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
46
52
|
class DockerComposeInstallationError(BinaryInstallError):
|
|
47
53
|
"""Raised when the Docker Compose installation fails."""
|
|
48
54
|
|
|
@@ -67,17 +73,29 @@ class DependencyError(Exception):
|
|
|
67
73
|
self.repo_link = repo_link
|
|
68
74
|
self.branch = branch
|
|
69
75
|
|
|
76
|
+
def __str__(self) -> str:
|
|
77
|
+
return f"DependencyError: {self.repo_name} ({self.repo_link}) on {self.branch}"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class UnableToCloneDependencyError(DependencyError):
|
|
81
|
+
"""Raised when a dependency is unable to be cloned."""
|
|
82
|
+
|
|
83
|
+
def __str__(self) -> str:
|
|
84
|
+
return f"Unable to clone dependency: {self.repo_name} ({self.repo_link}) on {self.branch}"
|
|
85
|
+
|
|
70
86
|
|
|
71
87
|
class InvalidDependencyConfigError(DependencyError):
|
|
72
88
|
"""Raised when a dependency's config is invalid."""
|
|
73
89
|
|
|
74
|
-
|
|
90
|
+
def __str__(self) -> str:
|
|
91
|
+
return f"Invalid config for dependency: {self.repo_name} ({self.repo_link}) on {self.branch}"
|
|
75
92
|
|
|
76
93
|
|
|
77
94
|
class DependencyNotInstalledError(DependencyError):
|
|
78
95
|
"""Raised when a dependency is not installed correctly."""
|
|
79
96
|
|
|
80
|
-
|
|
97
|
+
def __str__(self) -> str:
|
|
98
|
+
return f"Dependency not installed correctly: {self.repo_name} ({self.repo_link}) on {self.branch}"
|
|
81
99
|
|
|
82
100
|
|
|
83
101
|
class GitConfigError(Exception):
|
|
@@ -11,11 +11,14 @@ from sentry_sdk.integrations.argv import ArgvIntegration
|
|
|
11
11
|
from devservices.commands import list_dependencies
|
|
12
12
|
from devservices.commands import list_services
|
|
13
13
|
from devservices.commands import logs
|
|
14
|
+
from devservices.commands import purge
|
|
14
15
|
from devservices.commands import start
|
|
15
16
|
from devservices.commands import status
|
|
16
17
|
from devservices.commands import stop
|
|
17
18
|
from devservices.commands import update
|
|
18
19
|
from devservices.commands.check_for_update import check_for_update
|
|
20
|
+
from devservices.exceptions import DockerComposeInstallationError
|
|
21
|
+
from devservices.exceptions import DockerDaemonNotRunningError
|
|
19
22
|
from devservices.utils.docker_compose import check_docker_compose_version
|
|
20
23
|
|
|
21
24
|
sentry_environment = (
|
|
@@ -41,7 +44,14 @@ def cleanup() -> None:
|
|
|
41
44
|
|
|
42
45
|
|
|
43
46
|
def main() -> None:
|
|
44
|
-
|
|
47
|
+
try:
|
|
48
|
+
check_docker_compose_version()
|
|
49
|
+
except DockerDaemonNotRunningError as e:
|
|
50
|
+
print(e)
|
|
51
|
+
exit(1)
|
|
52
|
+
except DockerComposeInstallationError:
|
|
53
|
+
print("Failed to ensure docker compose is installed and up-to-date")
|
|
54
|
+
exit(1)
|
|
45
55
|
parser = argparse.ArgumentParser(
|
|
46
56
|
prog="devservices",
|
|
47
57
|
description="CLI tool for managing service dependencies.",
|
|
@@ -61,6 +71,7 @@ def main() -> None:
|
|
|
61
71
|
status.add_parser(subparsers)
|
|
62
72
|
logs.add_parser(subparsers)
|
|
63
73
|
update.add_parser(subparsers)
|
|
74
|
+
purge.add_parser(subparsers)
|
|
64
75
|
|
|
65
76
|
args = parser.parse_args()
|
|
66
77
|
|
|
@@ -25,7 +25,11 @@ from devservices.exceptions import DependencyError
|
|
|
25
25
|
from devservices.exceptions import DependencyNotInstalledError
|
|
26
26
|
from devservices.exceptions import FailedToSetGitConfigError
|
|
27
27
|
from devservices.exceptions import InvalidDependencyConfigError
|
|
28
|
+
from devservices.exceptions import UnableToCloneDependencyError
|
|
28
29
|
from devservices.utils.file_lock import lock
|
|
30
|
+
from devservices.utils.services import find_matching_service
|
|
31
|
+
from devservices.utils.services import Service
|
|
32
|
+
from devservices.utils.state import State
|
|
29
33
|
|
|
30
34
|
|
|
31
35
|
@dataclass(frozen=True)
|
|
@@ -132,6 +136,25 @@ def verify_local_dependencies(dependencies: list[Dependency]) -> bool:
|
|
|
132
136
|
)
|
|
133
137
|
|
|
134
138
|
|
|
139
|
+
def get_non_shared_remote_dependencies(
|
|
140
|
+
service_to_stop: Service, remote_dependencies: set[InstalledRemoteDependency]
|
|
141
|
+
) -> set[InstalledRemoteDependency]:
|
|
142
|
+
state = State()
|
|
143
|
+
started_services = state.get_started_services()
|
|
144
|
+
# We don't care about the remote dependencies of the service we are stopping
|
|
145
|
+
started_services.remove(service_to_stop.name)
|
|
146
|
+
other_running_remote_dependencies: set[InstalledRemoteDependency] = set()
|
|
147
|
+
for service_name in started_services:
|
|
148
|
+
service = find_matching_service(service_name)
|
|
149
|
+
# TODO: There is an edge case here where there is a shared remote dependency with different modes
|
|
150
|
+
other_running_remote_dependencies = other_running_remote_dependencies.union(
|
|
151
|
+
get_installed_remote_dependencies(
|
|
152
|
+
list(service.config.dependencies.values())
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
return remote_dependencies.difference(other_running_remote_dependencies)
|
|
156
|
+
|
|
157
|
+
|
|
135
158
|
def get_installed_remote_dependencies(
|
|
136
159
|
dependencies: list[Dependency],
|
|
137
160
|
) -> set[InstalledRemoteDependency]:
|
|
@@ -344,7 +367,7 @@ def _checkout_dependency(
|
|
|
344
367
|
cwd=temp_dir,
|
|
345
368
|
)
|
|
346
369
|
except subprocess.CalledProcessError as e:
|
|
347
|
-
raise
|
|
370
|
+
raise UnableToCloneDependencyError(
|
|
348
371
|
repo_name=dependency.repo_name,
|
|
349
372
|
repo_link=dependency.repo_link,
|
|
350
373
|
branch=dependency.branch,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
from devservices.exceptions import DockerDaemonNotRunningError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def check_docker_daemon_running() -> None:
|
|
9
|
+
try:
|
|
10
|
+
subprocess.run(
|
|
11
|
+
["docker", "info"],
|
|
12
|
+
capture_output=True,
|
|
13
|
+
text=True,
|
|
14
|
+
check=True,
|
|
15
|
+
)
|
|
16
|
+
except subprocess.CalledProcessError as e:
|
|
17
|
+
raise DockerDaemonNotRunningError(
|
|
18
|
+
"Unable to connect to the docker daemon. Is the docker daemon running?"
|
|
19
|
+
) from e
|
|
@@ -22,28 +22,15 @@ from devservices.exceptions import BinaryInstallError
|
|
|
22
22
|
from devservices.exceptions import DockerComposeError
|
|
23
23
|
from devservices.exceptions import DockerComposeInstallationError
|
|
24
24
|
from devservices.utils.dependencies import get_installed_remote_dependencies
|
|
25
|
+
from devservices.utils.dependencies import get_non_shared_remote_dependencies
|
|
25
26
|
from devservices.utils.dependencies import install_dependencies
|
|
26
27
|
from devservices.utils.dependencies import InstalledRemoteDependency
|
|
27
28
|
from devservices.utils.dependencies import verify_local_dependencies
|
|
29
|
+
from devservices.utils.docker import check_docker_daemon_running
|
|
28
30
|
from devservices.utils.install_binary import install_binary
|
|
29
31
|
from devservices.utils.services import Service
|
|
30
32
|
|
|
31
33
|
|
|
32
|
-
def get_active_docker_compose_projects() -> list[str]:
|
|
33
|
-
cmd = ["docker", "compose", "ls", "-q"]
|
|
34
|
-
try:
|
|
35
|
-
running_projects = subprocess.check_output(cmd, text=True)
|
|
36
|
-
except subprocess.CalledProcessError as e:
|
|
37
|
-
raise DockerComposeError(
|
|
38
|
-
command=" ".join(cmd),
|
|
39
|
-
returncode=e.returncode,
|
|
40
|
-
stdout=e.stdout,
|
|
41
|
-
stderr=e.stderr,
|
|
42
|
-
)
|
|
43
|
-
# docker compose ls always returns newline delimited string with an extra newline at the end
|
|
44
|
-
return running_projects.split("\n")[:-1]
|
|
45
|
-
|
|
46
|
-
|
|
47
34
|
def install_docker_compose() -> None:
|
|
48
35
|
# Determine the platform
|
|
49
36
|
system = platform.system()
|
|
@@ -104,11 +91,12 @@ def install_docker_compose() -> None:
|
|
|
104
91
|
|
|
105
92
|
|
|
106
93
|
def check_docker_compose_version() -> None:
|
|
107
|
-
|
|
94
|
+
# Throw an error if docker daemon isn't running
|
|
95
|
+
check_docker_daemon_running()
|
|
108
96
|
try:
|
|
109
97
|
# Run the docker compose version command
|
|
110
98
|
result = subprocess.run(
|
|
111
|
-
|
|
99
|
+
["docker", "compose", "version", "--short"],
|
|
112
100
|
capture_output=True,
|
|
113
101
|
text=True,
|
|
114
102
|
check=True,
|
|
@@ -224,11 +212,12 @@ def _get_docker_compose_commands_to_run(
|
|
|
224
212
|
service_config_file_path, current_env
|
|
225
213
|
)
|
|
226
214
|
services_to_use = non_remote_services.intersection(set(mode_dependencies))
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
215
|
+
if len(services_to_use) > 0:
|
|
216
|
+
docker_compose_commands.append(
|
|
217
|
+
create_docker_compose_command(
|
|
218
|
+
service.name, service_config_file_path, services_to_use
|
|
219
|
+
)
|
|
230
220
|
)
|
|
231
|
-
)
|
|
232
221
|
return docker_compose_commands
|
|
233
222
|
|
|
234
223
|
|
|
@@ -250,6 +239,11 @@ def run_docker_compose_command(
|
|
|
250
239
|
remote_dependencies = install_dependencies(dependencies)
|
|
251
240
|
else:
|
|
252
241
|
remote_dependencies = get_installed_remote_dependencies(dependencies)
|
|
242
|
+
# TODO: Refactor this to be more generic instead of having a one-off case for stopping
|
|
243
|
+
if command == "down":
|
|
244
|
+
remote_dependencies = get_non_shared_remote_dependencies(
|
|
245
|
+
service, remote_dependencies
|
|
246
|
+
)
|
|
253
247
|
relative_local_dependency_directory = os.path.relpath(
|
|
254
248
|
os.path.join(DEVSERVICES_DEPENDENCIES_CACHE_DIR, DEPENDENCY_CONFIG_VERSION),
|
|
255
249
|
service.repo_path,
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sqlite3
|
|
5
|
+
|
|
6
|
+
from devservices.constants import DEVSERVICES_LOCAL_DIR
|
|
7
|
+
from devservices.constants import STATE_DB_FILE
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class State:
|
|
11
|
+
_instance: State | None = None
|
|
12
|
+
state_db_file: str
|
|
13
|
+
conn: sqlite3.Connection
|
|
14
|
+
|
|
15
|
+
def __new__(cls) -> State:
|
|
16
|
+
if cls._instance is None:
|
|
17
|
+
cls._instance = super(State, cls).__new__(cls)
|
|
18
|
+
if not os.path.exists(DEVSERVICES_LOCAL_DIR):
|
|
19
|
+
os.makedirs(DEVSERVICES_LOCAL_DIR)
|
|
20
|
+
cls._instance.state_db_file = STATE_DB_FILE
|
|
21
|
+
cls._instance.conn = sqlite3.connect(cls._instance.state_db_file)
|
|
22
|
+
cls._instance.initialize_database()
|
|
23
|
+
return cls._instance
|
|
24
|
+
|
|
25
|
+
def initialize_database(self) -> None:
|
|
26
|
+
cursor = self.conn.cursor()
|
|
27
|
+
cursor.execute(
|
|
28
|
+
"""
|
|
29
|
+
CREATE TABLE IF NOT EXISTS started_services (
|
|
30
|
+
service_name TEXT PRIMARY KEY,
|
|
31
|
+
mode TEXT,
|
|
32
|
+
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
33
|
+
)
|
|
34
|
+
"""
|
|
35
|
+
)
|
|
36
|
+
self.conn.commit()
|
|
37
|
+
|
|
38
|
+
def add_started_service(self, service_name: str, mode: str) -> None:
|
|
39
|
+
cursor = self.conn.cursor()
|
|
40
|
+
started_services = self.get_started_services()
|
|
41
|
+
if service_name in started_services:
|
|
42
|
+
return
|
|
43
|
+
cursor.execute(
|
|
44
|
+
"""
|
|
45
|
+
INSERT INTO started_services (service_name, mode) VALUES (?, ?)
|
|
46
|
+
""",
|
|
47
|
+
(service_name, mode),
|
|
48
|
+
)
|
|
49
|
+
self.conn.commit()
|
|
50
|
+
|
|
51
|
+
def remove_started_service(self, service_name: str) -> None:
|
|
52
|
+
cursor = self.conn.cursor()
|
|
53
|
+
cursor.execute(
|
|
54
|
+
"""
|
|
55
|
+
DELETE FROM started_services WHERE service_name = ?
|
|
56
|
+
""",
|
|
57
|
+
(service_name,),
|
|
58
|
+
)
|
|
59
|
+
self.conn.commit()
|
|
60
|
+
|
|
61
|
+
def get_started_services(self) -> list[str]:
|
|
62
|
+
cursor = self.conn.cursor()
|
|
63
|
+
cursor.execute(
|
|
64
|
+
"""
|
|
65
|
+
SELECT service_name FROM started_services
|
|
66
|
+
"""
|
|
67
|
+
)
|
|
68
|
+
return [row[0] for row in cursor.fetchall()]
|
|
69
|
+
|
|
70
|
+
def get_mode_for_service(self, service_name: str) -> str | None:
|
|
71
|
+
cursor = self.conn.cursor()
|
|
72
|
+
cursor.execute(
|
|
73
|
+
"""
|
|
74
|
+
SELECT mode FROM started_services WHERE service_name = ?
|
|
75
|
+
""",
|
|
76
|
+
(service_name,),
|
|
77
|
+
)
|
|
78
|
+
result = cursor.fetchone()
|
|
79
|
+
if result is None:
|
|
80
|
+
return None
|
|
81
|
+
return str(result[0])
|
|
@@ -17,6 +17,7 @@ devservices/commands/check_for_update.py
|
|
|
17
17
|
devservices/commands/list_dependencies.py
|
|
18
18
|
devservices/commands/list_services.py
|
|
19
19
|
devservices/commands/logs.py
|
|
20
|
+
devservices/commands/purge.py
|
|
20
21
|
devservices/commands/start.py
|
|
21
22
|
devservices/commands/status.py
|
|
22
23
|
devservices/commands/stop.py
|
|
@@ -26,19 +27,23 @@ devservices/utils/__init__.py
|
|
|
26
27
|
devservices/utils/console.py
|
|
27
28
|
devservices/utils/dependencies.py
|
|
28
29
|
devservices/utils/devenv.py
|
|
30
|
+
devservices/utils/docker.py
|
|
29
31
|
devservices/utils/docker_compose.py
|
|
30
32
|
devservices/utils/file_lock.py
|
|
31
33
|
devservices/utils/install_binary.py
|
|
32
34
|
devservices/utils/services.py
|
|
35
|
+
devservices/utils/state.py
|
|
33
36
|
testing/__init__.py
|
|
34
37
|
testing/utils.py
|
|
35
38
|
tests/__init__.py
|
|
36
39
|
tests/conftest.py
|
|
37
40
|
tests/commands/test_list_services.py
|
|
41
|
+
tests/commands/test_purge.py
|
|
38
42
|
tests/commands/test_start.py
|
|
39
43
|
tests/commands/test_stop.py
|
|
40
44
|
tests/commands/test_update.py
|
|
41
45
|
tests/configs/test_service_config.py
|
|
42
46
|
tests/utils/test_dependencies.py
|
|
43
47
|
tests/utils/test_docker_compose.py
|
|
44
|
-
tests/utils/test_install_binary.py
|
|
48
|
+
tests/utils/test_install_binary.py
|
|
49
|
+
tests/utils/test_state.py
|
|
@@ -7,20 +7,19 @@ from unittest import mock
|
|
|
7
7
|
import pytest
|
|
8
8
|
|
|
9
9
|
from devservices.commands.list_services import list_services
|
|
10
|
+
from devservices.utils.state import State
|
|
10
11
|
from testing.utils import create_config_file
|
|
11
12
|
|
|
12
13
|
|
|
13
|
-
@mock.patch(
|
|
14
|
-
"devservices.utils.docker_compose.subprocess.run",
|
|
15
|
-
return_value=Namespace(stdout="\nexample-service\n"),
|
|
16
|
-
)
|
|
17
14
|
def test_list_running_services(
|
|
18
|
-
|
|
15
|
+
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
19
16
|
) -> None:
|
|
20
17
|
with mock.patch(
|
|
21
18
|
"devservices.commands.list_services.get_coderoot",
|
|
22
19
|
return_value=str(tmp_path / "code"),
|
|
23
|
-
):
|
|
20
|
+
), mock.patch("devservices.utils.state.STATE_DB_FILE", str(tmp_path / "state")):
|
|
21
|
+
state = State()
|
|
22
|
+
state.add_started_service("example-service", "default")
|
|
24
23
|
config = {
|
|
25
24
|
"x-sentry-service-config": {
|
|
26
25
|
"version": 0.1,
|
|
@@ -52,17 +51,13 @@ def test_list_running_services(
|
|
|
52
51
|
)
|
|
53
52
|
|
|
54
53
|
|
|
55
|
-
|
|
56
|
-
"devservices.utils.docker_compose.subprocess.run",
|
|
57
|
-
return_value=Namespace(stdout="\nexample-service\n"),
|
|
58
|
-
)
|
|
59
|
-
def test_list_all_services(
|
|
60
|
-
mock_run: mock.Mock, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
61
|
-
) -> None:
|
|
54
|
+
def test_list_all_services(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
|
62
55
|
with mock.patch(
|
|
63
56
|
"devservices.commands.list_services.get_coderoot",
|
|
64
57
|
return_value=str(tmp_path / "code"),
|
|
65
|
-
):
|
|
58
|
+
), mock.patch("devservices.utils.state.STATE_DB_FILE", str(tmp_path / "state")):
|
|
59
|
+
state = State()
|
|
60
|
+
state.add_started_service("example-service", "default")
|
|
66
61
|
config = {
|
|
67
62
|
"x-sentry-service-config": {
|
|
68
63
|
"version": 0.1,
|