devservices 0.0.4__tar.gz → 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {devservices-0.0.4 → devservices-1.0.0}/PKG-INFO +1 -1
- {devservices-0.0.4 → devservices-1.0.0}/README.md +4 -9
- {devservices-0.0.4 → devservices-1.0.0}/devservices/commands/list_dependencies.py +6 -4
- {devservices-0.0.4 → devservices-1.0.0}/devservices/commands/list_services.py +14 -12
- devservices-1.0.0/devservices/commands/logs.py +68 -0
- devservices-1.0.0/devservices/commands/purge.py +50 -0
- {devservices-0.0.4 → devservices-1.0.0}/devservices/commands/start.py +32 -4
- {devservices-0.0.4 → devservices-1.0.0}/devservices/commands/status.py +28 -11
- devservices-1.0.0/devservices/commands/stop.py +78 -0
- {devservices-0.0.4 → devservices-1.0.0}/devservices/commands/update.py +13 -9
- {devservices-0.0.4 → devservices-1.0.0}/devservices/constants.py +3 -0
- {devservices-0.0.4 → devservices-1.0.0}/devservices/exceptions.py +22 -2
- {devservices-0.0.4 → devservices-1.0.0}/devservices/main.py +25 -4
- devservices-1.0.0/devservices/utils/console.py +125 -0
- {devservices-0.0.4 → devservices-1.0.0}/devservices/utils/dependencies.py +45 -13
- devservices-1.0.0/devservices/utils/docker.py +36 -0
- {devservices-0.0.4 → devservices-1.0.0}/devservices/utils/docker_compose.py +22 -41
- {devservices-0.0.4 → devservices-1.0.0}/devservices/utils/install_binary.py +9 -5
- {devservices-0.0.4 → devservices-1.0.0}/devservices/utils/services.py +4 -1
- devservices-1.0.0/devservices/utils/state.py +90 -0
- {devservices-0.0.4 → devservices-1.0.0}/devservices.egg-info/PKG-INFO +1 -1
- {devservices-0.0.4 → devservices-1.0.0}/devservices.egg-info/SOURCES.txt +7 -1
- {devservices-0.0.4 → devservices-1.0.0}/pyproject.toml +1 -1
- {devservices-0.0.4 → devservices-1.0.0}/tests/commands/test_list_services.py +9 -14
- devservices-1.0.0/tests/commands/test_purge.py +132 -0
- {devservices-0.0.4 → devservices-1.0.0}/tests/commands/test_start.py +64 -4
- {devservices-0.0.4 → devservices-1.0.0}/tests/commands/test_stop.py +27 -7
- {devservices-0.0.4 → devservices-1.0.0}/tests/commands/test_update.py +1 -3
- devservices-1.0.0/tests/conftest.py +10 -0
- {devservices-0.0.4 → devservices-1.0.0}/tests/utils/test_dependencies.py +227 -0
- devservices-1.0.0/tests/utils/test_docker.py +47 -0
- {devservices-0.0.4 → devservices-1.0.0}/tests/utils/test_docker_compose.py +87 -24
- devservices-1.0.0/tests/utils/test_state.py +54 -0
- devservices-0.0.4/devservices/commands/logs.py +0 -45
- devservices-0.0.4/devservices/commands/stop.py +0 -40
- devservices-0.0.4/devservices/utils/console.py +0 -67
- devservices-0.0.4/tests/conftest.py +0 -0
- {devservices-0.0.4 → devservices-1.0.0}/LICENSE.md +0 -0
- {devservices-0.0.4 → devservices-1.0.0}/devservices/__init__.py +0 -0
- {devservices-0.0.4 → devservices-1.0.0}/devservices/commands/__init__.py +0 -0
- {devservices-0.0.4 → devservices-1.0.0}/devservices/commands/check_for_update.py +0 -0
- {devservices-0.0.4 → devservices-1.0.0}/devservices/configs/service_config.py +0 -0
- {devservices-0.0.4 → devservices-1.0.0}/devservices/utils/__init__.py +0 -0
- {devservices-0.0.4 → devservices-1.0.0}/devservices/utils/devenv.py +0 -0
- {devservices-0.0.4 → devservices-1.0.0}/devservices/utils/file_lock.py +0 -0
- {devservices-0.0.4 → devservices-1.0.0}/devservices.egg-info/dependency_links.txt +0 -0
- {devservices-0.0.4 → devservices-1.0.0}/devservices.egg-info/entry_points.txt +0 -0
- {devservices-0.0.4 → devservices-1.0.0}/devservices.egg-info/requires.txt +0 -0
- {devservices-0.0.4 → devservices-1.0.0}/devservices.egg-info/top_level.txt +0 -0
- {devservices-0.0.4 → devservices-1.0.0}/setup.cfg +0 -0
- {devservices-0.0.4 → devservices-1.0.0}/testing/__init__.py +0 -0
- {devservices-0.0.4 → devservices-1.0.0}/testing/utils.py +0 -0
- {devservices-0.0.4 → devservices-1.0.0}/tests/__init__.py +0 -0
- {devservices-0.0.4 → devservices-1.0.0}/tests/configs/test_service_config.py +0 -0
- {devservices-0.0.4 → devservices-1.0.0}/tests/utils/test_install_binary.py +0 -0
|
@@ -8,20 +8,13 @@ A standalone cli tool used to manage dependencies for services. It simplifies th
|
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
The recommended way to install devservices is through a virtualenv in the requirements.txt.
|
|
12
12
|
|
|
13
13
|
```
|
|
14
|
-
|
|
15
|
-
INSTALL_DIR="$HOME/.local/bin"
|
|
16
|
-
curl -L "https://github.com/getsentry/devservices/releases/download/0.0.4/devservices-$PLATFORM" -o "$INSTALL_DIR/devservices"
|
|
17
|
-
chmod +x "$INSTALL_DIR/devservices"
|
|
14
|
+
devservices==1.0.0
|
|
18
15
|
```
|
|
19
16
|
|
|
20
|
-
Alternatively, if the repository you're working in has a python virtualenv, you can simply add this to the requirements-dev.txt:
|
|
21
17
|
|
|
22
|
-
```
|
|
23
|
-
devservices==0.0.4
|
|
24
|
-
```
|
|
25
18
|
## Usage
|
|
26
19
|
|
|
27
20
|
devservices provides several commands to manage your services:
|
|
@@ -36,3 +29,5 @@ NOTE: service-name is an optional parameter. If not provided, devservices will a
|
|
|
36
29
|
- `devservices logs <service-name>`: View logs for a specific service.
|
|
37
30
|
- `devservices list-services`: List all available Sentry services.
|
|
38
31
|
- `devservices list-dependencies <service-name>`: List all dependencies for a service and whether they are enabled/disabled.
|
|
32
|
+
- `devservices update` Update devservices to the latest version.
|
|
33
|
+
- `devservices purge`: Purge the local devservices cache.
|
|
@@ -4,6 +4,7 @@ from argparse import _SubParsersAction
|
|
|
4
4
|
from argparse import ArgumentParser
|
|
5
5
|
from argparse import Namespace
|
|
6
6
|
|
|
7
|
+
from devservices.utils.console import Console
|
|
7
8
|
from devservices.utils.services import find_matching_service
|
|
8
9
|
|
|
9
10
|
|
|
@@ -22,20 +23,21 @@ def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
|
22
23
|
|
|
23
24
|
def list_dependencies(args: Namespace) -> None:
|
|
24
25
|
"""List the dependencies of a service."""
|
|
26
|
+
console = Console()
|
|
25
27
|
service_name = args.service_name
|
|
26
28
|
|
|
27
29
|
try:
|
|
28
30
|
service = find_matching_service(service_name)
|
|
29
31
|
except Exception as e:
|
|
30
|
-
|
|
32
|
+
console.failure(str(e))
|
|
31
33
|
exit(1)
|
|
32
34
|
|
|
33
35
|
dependencies = service.config.dependencies
|
|
34
36
|
|
|
35
37
|
if not dependencies:
|
|
36
|
-
|
|
38
|
+
console.info(f"No dependencies found for {service.name}")
|
|
37
39
|
return
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
console.info(f"Dependencies of {service.name}:")
|
|
40
42
|
for dependency_key, dependency_info in dependencies.items():
|
|
41
|
-
|
|
43
|
+
console.info("-" + dependency_key + ":" + dependency_info.description)
|
|
@@ -4,9 +4,10 @@ from argparse import _SubParsersAction
|
|
|
4
4
|
from argparse import ArgumentParser
|
|
5
5
|
from argparse import Namespace
|
|
6
6
|
|
|
7
|
+
from devservices.utils.console import Console
|
|
7
8
|
from devservices.utils.devenv import get_coderoot
|
|
8
|
-
from devservices.utils.docker_compose import get_active_docker_compose_projects
|
|
9
9
|
from devservices.utils.services import get_local_services
|
|
10
|
+
from devservices.utils.state import State
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
@@ -24,34 +25,35 @@ def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
|
24
25
|
|
|
25
26
|
def list_services(args: Namespace) -> None:
|
|
26
27
|
"""List the services installed locally."""
|
|
27
|
-
|
|
28
|
+
console = Console()
|
|
28
29
|
# Get all of the services installed locally
|
|
29
30
|
coderoot = get_coderoot()
|
|
30
31
|
services = get_local_services(coderoot)
|
|
31
|
-
|
|
32
|
+
state = State()
|
|
33
|
+
running_services = state.get_started_services()
|
|
32
34
|
|
|
33
35
|
if not services:
|
|
34
|
-
|
|
36
|
+
console.warning("No services found")
|
|
35
37
|
return
|
|
36
38
|
|
|
37
39
|
services_to_show = (
|
|
38
|
-
services if args.all else [s for s in services if s.name in
|
|
40
|
+
services if args.all else [s for s in services if s.name in running_services]
|
|
39
41
|
)
|
|
40
42
|
|
|
41
43
|
if args.all:
|
|
42
|
-
|
|
44
|
+
console.info("Services installed locally:")
|
|
43
45
|
else:
|
|
44
|
-
|
|
46
|
+
console.info("Running services:")
|
|
45
47
|
|
|
46
48
|
for service in services_to_show:
|
|
47
|
-
status = "running" if service.name in
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
status = "running" if service.name in running_services else "stopped"
|
|
50
|
+
console.info(f"- {service.name}")
|
|
51
|
+
console.info(f" status: {status}")
|
|
52
|
+
console.info(f" location: {service.repo_path}")
|
|
51
53
|
|
|
52
54
|
if not args.all:
|
|
53
55
|
stopped_count = len(services) - len(services_to_show)
|
|
54
56
|
if stopped_count > 0:
|
|
55
|
-
|
|
57
|
+
console.info(
|
|
56
58
|
f"\n{stopped_count} stopped service(s) not shown. Use --all/-a to see them."
|
|
57
59
|
)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from argparse import _SubParsersAction
|
|
4
|
+
from argparse import ArgumentParser
|
|
5
|
+
from argparse import Namespace
|
|
6
|
+
|
|
7
|
+
from devservices.constants import MAX_LOG_LINES
|
|
8
|
+
from devservices.exceptions import DependencyError
|
|
9
|
+
from devservices.exceptions import DockerComposeError
|
|
10
|
+
from devservices.utils.console import Console
|
|
11
|
+
from devservices.utils.dependencies import install_and_verify_dependencies
|
|
12
|
+
from devservices.utils.docker_compose import run_docker_compose_command
|
|
13
|
+
from devservices.utils.services import find_matching_service
|
|
14
|
+
from devservices.utils.state import State
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
18
|
+
parser = subparsers.add_parser("logs", help="View logs for a service")
|
|
19
|
+
parser.add_argument(
|
|
20
|
+
"service_name",
|
|
21
|
+
help="Name of the service to view logs for",
|
|
22
|
+
nargs="?",
|
|
23
|
+
default=None,
|
|
24
|
+
)
|
|
25
|
+
parser.set_defaults(func=logs)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def logs(args: Namespace) -> None:
|
|
29
|
+
"""View the logs for a specified service."""
|
|
30
|
+
console = Console()
|
|
31
|
+
service_name = args.service_name
|
|
32
|
+
try:
|
|
33
|
+
service = find_matching_service(service_name)
|
|
34
|
+
except Exception as e:
|
|
35
|
+
console.failure(str(e))
|
|
36
|
+
exit(1)
|
|
37
|
+
|
|
38
|
+
modes = service.config.modes
|
|
39
|
+
# TODO: allow custom modes to be used
|
|
40
|
+
mode_to_use = "default"
|
|
41
|
+
mode_dependencies = modes[mode_to_use]
|
|
42
|
+
|
|
43
|
+
state = State()
|
|
44
|
+
running_services = state.get_started_services()
|
|
45
|
+
if service_name not in running_services:
|
|
46
|
+
console.warning(f"Service {service_name} is not running")
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
remote_dependencies = install_and_verify_dependencies(service)
|
|
51
|
+
except DependencyError as de:
|
|
52
|
+
console.failure(str(de))
|
|
53
|
+
exit(1)
|
|
54
|
+
try:
|
|
55
|
+
logs_output = run_docker_compose_command(
|
|
56
|
+
service,
|
|
57
|
+
"logs",
|
|
58
|
+
mode_dependencies,
|
|
59
|
+
remote_dependencies,
|
|
60
|
+
options=["-n", MAX_LOG_LINES],
|
|
61
|
+
)
|
|
62
|
+
except DockerComposeError as dce:
|
|
63
|
+
console.failure(f"Failed to get logs for {service.name}: {dce.stderr}")
|
|
64
|
+
exit(1)
|
|
65
|
+
for log in logs_output:
|
|
66
|
+
log_stdout: str | None = log.stdout
|
|
67
|
+
if log_stdout is not None:
|
|
68
|
+
console.info(log_stdout)
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
from devservices.exceptions import DockerDaemonNotRunningError
|
|
11
|
+
from devservices.utils.console import Console
|
|
12
|
+
from devservices.utils.console import Status
|
|
13
|
+
from devservices.utils.docker import stop_all_running_containers
|
|
14
|
+
from devservices.utils.state import State
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
18
|
+
parser = subparsers.add_parser("purge", help="Purge the local devservices cache")
|
|
19
|
+
parser.set_defaults(func=purge)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def purge(args: Namespace) -> None:
|
|
23
|
+
"""Purge the local devservices cache."""
|
|
24
|
+
console = Console()
|
|
25
|
+
# Prompt the user to stop all running containers
|
|
26
|
+
should_stop_containers = console.confirm(
|
|
27
|
+
"Warning: Purging stops all running containers and clears devservices state. Would you like to continue?"
|
|
28
|
+
)
|
|
29
|
+
if not should_stop_containers:
|
|
30
|
+
console.warning("Purge canceled")
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
if os.path.exists(DEVSERVICES_CACHE_DIR):
|
|
34
|
+
try:
|
|
35
|
+
shutil.rmtree(DEVSERVICES_CACHE_DIR)
|
|
36
|
+
except PermissionError as e:
|
|
37
|
+
console.failure(f"Failed to purge cache: {e}")
|
|
38
|
+
exit(1)
|
|
39
|
+
state = State()
|
|
40
|
+
state.clear_state()
|
|
41
|
+
with Status(
|
|
42
|
+
lambda: console.warning("Stopping all running containers"),
|
|
43
|
+
lambda: console.success("All running containers have been stopped"),
|
|
44
|
+
):
|
|
45
|
+
try:
|
|
46
|
+
stop_all_running_containers()
|
|
47
|
+
except DockerDaemonNotRunningError:
|
|
48
|
+
console.warning("The docker daemon not running, no containers to stop")
|
|
49
|
+
|
|
50
|
+
console.success("The local devservices cache and state has been purged")
|
|
@@ -4,10 +4,14 @@ 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
|
|
9
|
+
from devservices.utils.console import Console
|
|
8
10
|
from devservices.utils.console import Status
|
|
11
|
+
from devservices.utils.dependencies import install_and_verify_dependencies
|
|
9
12
|
from devservices.utils.docker_compose import run_docker_compose_command
|
|
10
13
|
from devservices.utils.services import find_matching_service
|
|
14
|
+
from devservices.utils.state import State
|
|
11
15
|
|
|
12
16
|
|
|
13
17
|
def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
@@ -15,16 +19,23 @@ def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
|
15
19
|
parser.add_argument(
|
|
16
20
|
"service_name", help="Name of the service to start", nargs="?", default=None
|
|
17
21
|
)
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--debug",
|
|
24
|
+
help="Enable debug mode",
|
|
25
|
+
action="store_true",
|
|
26
|
+
default=False,
|
|
27
|
+
)
|
|
18
28
|
parser.set_defaults(func=start)
|
|
19
29
|
|
|
20
30
|
|
|
21
31
|
def start(args: Namespace) -> None:
|
|
22
32
|
"""Start a service and its dependencies."""
|
|
33
|
+
console = Console()
|
|
23
34
|
service_name = args.service_name
|
|
24
35
|
try:
|
|
25
36
|
service = find_matching_service(service_name)
|
|
26
37
|
except Exception as e:
|
|
27
|
-
|
|
38
|
+
console.failure(str(e))
|
|
28
39
|
exit(1)
|
|
29
40
|
|
|
30
41
|
modes = service.config.modes
|
|
@@ -32,11 +43,28 @@ def start(args: Namespace) -> None:
|
|
|
32
43
|
mode_to_start = "default"
|
|
33
44
|
mode_dependencies = modes[mode_to_start]
|
|
34
45
|
|
|
35
|
-
with Status(
|
|
46
|
+
with Status(
|
|
47
|
+
lambda: console.warning(f"Starting {service.name}"),
|
|
48
|
+
lambda: console.success(f"{service.name} started"),
|
|
49
|
+
) as status:
|
|
50
|
+
try:
|
|
51
|
+
remote_dependencies = install_and_verify_dependencies(
|
|
52
|
+
service, force_update_dependencies=True
|
|
53
|
+
)
|
|
54
|
+
except DependencyError as de:
|
|
55
|
+
status.failure(str(de))
|
|
56
|
+
exit(1)
|
|
36
57
|
try:
|
|
37
58
|
run_docker_compose_command(
|
|
38
|
-
service,
|
|
59
|
+
service,
|
|
60
|
+
"up",
|
|
61
|
+
mode_dependencies,
|
|
62
|
+
remote_dependencies,
|
|
63
|
+
options=["-d"],
|
|
39
64
|
)
|
|
40
65
|
except DockerComposeError as dce:
|
|
41
|
-
status.
|
|
66
|
+
status.failure(f"Failed to start {service.name}: {dce.stderr}")
|
|
42
67
|
exit(1)
|
|
68
|
+
# TODO: We should factor in healthchecks here before marking service as running
|
|
69
|
+
state = State()
|
|
70
|
+
state.add_started_service(service.name, mode_to_start)
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
-
import sys
|
|
5
4
|
from argparse import _SubParsersAction
|
|
6
5
|
from argparse import ArgumentParser
|
|
7
6
|
from argparse import Namespace
|
|
8
7
|
|
|
8
|
+
from devservices.exceptions import DependencyError
|
|
9
9
|
from devservices.exceptions import DockerComposeError
|
|
10
|
+
from devservices.utils.console import Console
|
|
11
|
+
from devservices.utils.dependencies import install_and_verify_dependencies
|
|
10
12
|
from devservices.utils.docker_compose import run_docker_compose_command
|
|
11
13
|
from devservices.utils.services import find_matching_service
|
|
12
14
|
|
|
@@ -33,11 +35,13 @@ def format_status_output(status_json: str) -> str:
|
|
|
33
35
|
service = json.loads(service_status)
|
|
34
36
|
name = service["Service"]
|
|
35
37
|
state = service["State"]
|
|
38
|
+
container_name = service["Name"]
|
|
36
39
|
health = service.get("Health", "N/A")
|
|
37
40
|
ports = service.get("Publishers", [])
|
|
38
41
|
running_for = service.get("RunningFor", "N/A")
|
|
39
42
|
|
|
40
43
|
output.append(f"{name}")
|
|
44
|
+
output.append(f"Container: {container_name}")
|
|
41
45
|
output.append(f"Status: {state}")
|
|
42
46
|
output.append(f"Health: {health}")
|
|
43
47
|
output.append(f"Uptime: {running_for}")
|
|
@@ -58,11 +62,12 @@ def format_status_output(status_json: str) -> str:
|
|
|
58
62
|
|
|
59
63
|
def status(args: Namespace) -> None:
|
|
60
64
|
"""Start a service and its dependencies."""
|
|
65
|
+
console = Console()
|
|
61
66
|
service_name = args.service_name
|
|
62
67
|
try:
|
|
63
68
|
service = find_matching_service(service_name)
|
|
64
69
|
except Exception as e:
|
|
65
|
-
|
|
70
|
+
console.failure(str(e))
|
|
66
71
|
exit(1)
|
|
67
72
|
|
|
68
73
|
modes = service.config.modes
|
|
@@ -71,19 +76,31 @@ def status(args: Namespace) -> None:
|
|
|
71
76
|
mode_dependencies = modes[mode_to_view]
|
|
72
77
|
|
|
73
78
|
try:
|
|
74
|
-
|
|
75
|
-
|
|
79
|
+
remote_dependencies = install_and_verify_dependencies(service)
|
|
80
|
+
except DependencyError as de:
|
|
81
|
+
console.failure(str(de))
|
|
82
|
+
exit(1)
|
|
83
|
+
try:
|
|
84
|
+
status_json_results = run_docker_compose_command(
|
|
85
|
+
service,
|
|
86
|
+
"ps",
|
|
87
|
+
mode_dependencies,
|
|
88
|
+
remote_dependencies,
|
|
89
|
+
options=["--format", "json"],
|
|
76
90
|
)
|
|
77
91
|
except DockerComposeError as dce:
|
|
78
|
-
|
|
92
|
+
console.failure(f"Failed to get status for {service.name}: {dce.stderr}")
|
|
79
93
|
exit(1)
|
|
80
|
-
|
|
81
|
-
if
|
|
82
|
-
|
|
94
|
+
|
|
95
|
+
# Filter out empty stdout to help us determine if the service is running
|
|
96
|
+
status_json_results = [
|
|
97
|
+
status_json for status_json in status_json_results if status_json.stdout
|
|
98
|
+
]
|
|
99
|
+
if len(status_json_results) == 0:
|
|
100
|
+
console.warning(f"{service.name} is not running")
|
|
83
101
|
return
|
|
84
102
|
output = f"Service: {service.name}\n\n"
|
|
85
|
-
for status_json in
|
|
103
|
+
for status_json in status_json_results:
|
|
86
104
|
output += format_status_output(status_json.stdout)
|
|
87
105
|
output += "=" * LINE_LENGTH
|
|
88
|
-
|
|
89
|
-
sys.stdout.flush()
|
|
106
|
+
console.info(output + "\n")
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from argparse import _SubParsersAction
|
|
4
|
+
from argparse import ArgumentParser
|
|
5
|
+
from argparse import Namespace
|
|
6
|
+
|
|
7
|
+
from devservices.exceptions import DependencyError
|
|
8
|
+
from devservices.exceptions import DockerComposeError
|
|
9
|
+
from devservices.utils.console import Console
|
|
10
|
+
from devservices.utils.console import Status
|
|
11
|
+
from devservices.utils.dependencies import get_non_shared_remote_dependencies
|
|
12
|
+
from devservices.utils.dependencies import install_and_verify_dependencies
|
|
13
|
+
from devservices.utils.docker_compose import run_docker_compose_command
|
|
14
|
+
from devservices.utils.services import find_matching_service
|
|
15
|
+
from devservices.utils.state import State
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
19
|
+
parser = subparsers.add_parser("stop", help="Stop a service and its dependencies")
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"service_name", help="Name of the service to stop", nargs="?", default=None
|
|
22
|
+
)
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"--debug",
|
|
25
|
+
help="Enable debug mode",
|
|
26
|
+
action="store_true",
|
|
27
|
+
default=False,
|
|
28
|
+
)
|
|
29
|
+
parser.set_defaults(func=stop)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def stop(args: Namespace) -> None:
|
|
33
|
+
"""Stop a service and its dependencies."""
|
|
34
|
+
console = Console()
|
|
35
|
+
service_name = args.service_name
|
|
36
|
+
try:
|
|
37
|
+
service = find_matching_service(service_name)
|
|
38
|
+
except Exception as e:
|
|
39
|
+
console.failure(str(e))
|
|
40
|
+
exit(1)
|
|
41
|
+
|
|
42
|
+
modes = service.config.modes
|
|
43
|
+
# TODO: allow custom modes to be used
|
|
44
|
+
mode_to_stop = "default"
|
|
45
|
+
mode_dependencies = modes[mode_to_stop]
|
|
46
|
+
|
|
47
|
+
state = State()
|
|
48
|
+
started_services = state.get_started_services()
|
|
49
|
+
if service.name not in started_services:
|
|
50
|
+
console.warning(f"{service.name} is not running")
|
|
51
|
+
exit(0)
|
|
52
|
+
|
|
53
|
+
with Status(
|
|
54
|
+
lambda: console.warning(f"Stopping {service.name}"),
|
|
55
|
+
lambda: console.success(f"{service.name} stopped"),
|
|
56
|
+
) as status:
|
|
57
|
+
try:
|
|
58
|
+
remote_dependencies = install_and_verify_dependencies(service)
|
|
59
|
+
except DependencyError as de:
|
|
60
|
+
status.failure(str(de))
|
|
61
|
+
exit(1)
|
|
62
|
+
remote_dependencies = get_non_shared_remote_dependencies(
|
|
63
|
+
service, remote_dependencies
|
|
64
|
+
)
|
|
65
|
+
try:
|
|
66
|
+
run_docker_compose_command(
|
|
67
|
+
service,
|
|
68
|
+
"down",
|
|
69
|
+
mode_dependencies,
|
|
70
|
+
remote_dependencies,
|
|
71
|
+
)
|
|
72
|
+
except DockerComposeError as dce:
|
|
73
|
+
status.failure(f"Failed to stop {service.name}: {dce.stderr}")
|
|
74
|
+
exit(1)
|
|
75
|
+
|
|
76
|
+
# TODO: We should factor in healthchecks here before marking service as stopped
|
|
77
|
+
state = State()
|
|
78
|
+
state.remove_started_service(service.name)
|
|
@@ -11,6 +11,7 @@ from devservices.commands.check_for_update import check_for_update
|
|
|
11
11
|
from devservices.constants import DEVSERVICES_DOWNLOAD_URL
|
|
12
12
|
from devservices.exceptions import BinaryInstallError
|
|
13
13
|
from devservices.exceptions import DevservicesUpdateError
|
|
14
|
+
from devservices.utils.console import Console
|
|
14
15
|
from devservices.utils.install_binary import install_binary
|
|
15
16
|
|
|
16
17
|
|
|
@@ -21,14 +22,16 @@ def is_in_virtualenv() -> bool:
|
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
def update_version(exec_path: str, latest_version: str) -> None:
|
|
25
|
+
console = Console()
|
|
24
26
|
system = platform.system().lower()
|
|
25
27
|
url = f"{DEVSERVICES_DOWNLOAD_URL}/{latest_version}/devservices-{system}"
|
|
26
28
|
try:
|
|
27
29
|
install_binary("devservices", exec_path, latest_version, url)
|
|
28
30
|
except BinaryInstallError as e:
|
|
29
|
-
|
|
31
|
+
console.failure(f"Failed to update devservices: {e}")
|
|
32
|
+
exit(1)
|
|
30
33
|
|
|
31
|
-
|
|
34
|
+
console.success(f"Devservices {latest_version} updated successfully")
|
|
32
35
|
|
|
33
36
|
|
|
34
37
|
def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
@@ -39,6 +42,7 @@ def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
|
39
42
|
|
|
40
43
|
|
|
41
44
|
def update(args: Namespace) -> None:
|
|
45
|
+
console = Console()
|
|
42
46
|
current_version = metadata.version("devservices")
|
|
43
47
|
latest_version = check_for_update(current_version)
|
|
44
48
|
|
|
@@ -46,21 +50,21 @@ def update(args: Namespace) -> None:
|
|
|
46
50
|
raise DevservicesUpdateError("Failed to check for updates.")
|
|
47
51
|
|
|
48
52
|
if latest_version == current_version:
|
|
49
|
-
|
|
53
|
+
console.warning("You're already on the latest version.")
|
|
50
54
|
return
|
|
51
55
|
|
|
52
|
-
|
|
56
|
+
console.warning(f"A new version of devservices is available: {latest_version}")
|
|
53
57
|
|
|
54
58
|
if is_in_virtualenv():
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
console.warning("You are running in a virtual environment.")
|
|
60
|
+
console.warning(
|
|
57
61
|
"To update, please update your requirements.txt or requirements-dev.txt file with the new version."
|
|
58
62
|
)
|
|
59
|
-
|
|
63
|
+
console.warning(
|
|
60
64
|
f"For example, update the line in requirements.txt to: devservices=={latest_version}"
|
|
61
65
|
)
|
|
62
|
-
|
|
66
|
+
console.warning("Then, run: pip install --update -r requirements.txt")
|
|
63
67
|
return
|
|
64
68
|
|
|
65
|
-
|
|
69
|
+
console.info("Upgrading to the latest version...")
|
|
66
70
|
update_version(sys.executable, latest_version)
|
|
@@ -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,5 @@ 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"
|
|
27
|
+
LOGGER_NAME = "devservices"
|
|
@@ -43,6 +43,14 @@ 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
|
+
def __str__(self) -> str:
|
|
50
|
+
# TODO: Provide explicit instructions on what to do
|
|
51
|
+
return "Unable to connect to the docker daemon. Is the docker daemon running?"
|
|
52
|
+
|
|
53
|
+
|
|
46
54
|
class DockerComposeInstallationError(BinaryInstallError):
|
|
47
55
|
"""Raised when the Docker Compose installation fails."""
|
|
48
56
|
|
|
@@ -67,17 +75,29 @@ class DependencyError(Exception):
|
|
|
67
75
|
self.repo_link = repo_link
|
|
68
76
|
self.branch = branch
|
|
69
77
|
|
|
78
|
+
def __str__(self) -> str:
|
|
79
|
+
return f"DependencyError: {self.repo_name} ({self.repo_link}) on {self.branch}"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class UnableToCloneDependencyError(DependencyError):
|
|
83
|
+
"""Raised when a dependency is unable to be cloned."""
|
|
84
|
+
|
|
85
|
+
def __str__(self) -> str:
|
|
86
|
+
return f"Unable to clone dependency: {self.repo_name} ({self.repo_link}) on {self.branch}"
|
|
87
|
+
|
|
70
88
|
|
|
71
89
|
class InvalidDependencyConfigError(DependencyError):
|
|
72
90
|
"""Raised when a dependency's config is invalid."""
|
|
73
91
|
|
|
74
|
-
|
|
92
|
+
def __str__(self) -> str:
|
|
93
|
+
return f"Invalid config for dependency: {self.repo_name} ({self.repo_link}) on {self.branch}"
|
|
75
94
|
|
|
76
95
|
|
|
77
96
|
class DependencyNotInstalledError(DependencyError):
|
|
78
97
|
"""Raised when a dependency is not installed correctly."""
|
|
79
98
|
|
|
80
|
-
|
|
99
|
+
def __str__(self) -> str:
|
|
100
|
+
return f"Dependency not installed correctly: {self.repo_name} ({self.repo_link}) on {self.branch}"
|
|
81
101
|
|
|
82
102
|
|
|
83
103
|
class GitConfigError(Exception):
|