devservices 1.0.5__tar.gz → 1.0.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {devservices-1.0.5 → devservices-1.0.7}/PKG-INFO +1 -1
- {devservices-1.0.5 → devservices-1.0.7}/README.md +1 -1
- {devservices-1.0.5 → devservices-1.0.7}/devservices/commands/down.py +12 -7
- {devservices-1.0.5 → devservices-1.0.7}/devservices/commands/logs.py +1 -1
- devservices-1.0.7/devservices/commands/purge.py +94 -0
- {devservices-1.0.5 → devservices-1.0.7}/devservices/commands/status.py +1 -1
- {devservices-1.0.5 → devservices-1.0.7}/devservices/commands/up.py +33 -6
- {devservices-1.0.5 → devservices-1.0.7}/devservices/commands/update.py +1 -1
- {devservices-1.0.5 → devservices-1.0.7}/devservices/constants.py +13 -1
- {devservices-1.0.5 → devservices-1.0.7}/devservices/exceptions.py +19 -2
- {devservices-1.0.5 → devservices-1.0.7}/devservices/main.py +2 -2
- devservices-1.0.7/devservices/utils/check_for_update.py +59 -0
- {devservices-1.0.5 → devservices-1.0.7}/devservices/utils/dependencies.py +49 -0
- devservices-1.0.7/devservices/utils/docker.py +219 -0
- {devservices-1.0.5 → devservices-1.0.7}/devservices/utils/docker_compose.py +44 -5
- {devservices-1.0.5 → devservices-1.0.7}/devservices.egg-info/PKG-INFO +1 -1
- {devservices-1.0.5 → devservices-1.0.7}/devservices.egg-info/SOURCES.txt +2 -1
- {devservices-1.0.5 → devservices-1.0.7}/pyproject.toml +1 -1
- devservices-1.0.7/tests/commands/test_purge.py +517 -0
- {devservices-1.0.5 → devservices-1.0.7}/tests/commands/test_up.py +275 -2
- devservices-1.0.7/tests/utils/test_check_for_update.py +170 -0
- {devservices-1.0.5 → devservices-1.0.7}/tests/utils/test_dependencies.py +48 -0
- devservices-1.0.7/tests/utils/test_docker.py +493 -0
- {devservices-1.0.5 → devservices-1.0.7}/tests/utils/test_docker_compose.py +165 -99
- devservices-1.0.5/devservices/commands/check_for_update.py +0 -14
- devservices-1.0.5/devservices/commands/purge.py +0 -81
- devservices-1.0.5/devservices/utils/docker.py +0 -36
- devservices-1.0.5/tests/commands/test_purge.py +0 -167
- devservices-1.0.5/tests/utils/test_docker.py +0 -47
- {devservices-1.0.5 → devservices-1.0.7}/LICENSE.md +0 -0
- {devservices-1.0.5 → devservices-1.0.7}/devservices/__init__.py +0 -0
- {devservices-1.0.5 → devservices-1.0.7}/devservices/commands/__init__.py +0 -0
- {devservices-1.0.5 → devservices-1.0.7}/devservices/commands/list_dependencies.py +0 -0
- {devservices-1.0.5 → devservices-1.0.7}/devservices/commands/list_services.py +0 -0
- {devservices-1.0.5 → devservices-1.0.7}/devservices/configs/service_config.py +0 -0
- {devservices-1.0.5 → devservices-1.0.7}/devservices/utils/__init__.py +0 -0
- {devservices-1.0.5 → devservices-1.0.7}/devservices/utils/console.py +0 -0
- {devservices-1.0.5 → devservices-1.0.7}/devservices/utils/devenv.py +0 -0
- {devservices-1.0.5 → devservices-1.0.7}/devservices/utils/file_lock.py +0 -0
- {devservices-1.0.5 → devservices-1.0.7}/devservices/utils/install_binary.py +0 -0
- {devservices-1.0.5 → devservices-1.0.7}/devservices/utils/services.py +0 -0
- {devservices-1.0.5 → devservices-1.0.7}/devservices/utils/state.py +0 -0
- {devservices-1.0.5 → devservices-1.0.7}/devservices.egg-info/dependency_links.txt +0 -0
- {devservices-1.0.5 → devservices-1.0.7}/devservices.egg-info/entry_points.txt +0 -0
- {devservices-1.0.5 → devservices-1.0.7}/devservices.egg-info/requires.txt +0 -0
- {devservices-1.0.5 → devservices-1.0.7}/devservices.egg-info/top_level.txt +0 -0
- {devservices-1.0.5 → devservices-1.0.7}/setup.cfg +0 -0
- {devservices-1.0.5 → devservices-1.0.7}/testing/__init__.py +0 -0
- {devservices-1.0.5 → devservices-1.0.7}/testing/utils.py +0 -0
- {devservices-1.0.5 → devservices-1.0.7}/tests/__init__.py +0 -0
- {devservices-1.0.5 → devservices-1.0.7}/tests/commands/test_down.py +0 -0
- {devservices-1.0.5 → devservices-1.0.7}/tests/commands/test_list_dependencies.py +0 -0
- {devservices-1.0.5 → devservices-1.0.7}/tests/commands/test_list_services.py +0 -0
- {devservices-1.0.5 → devservices-1.0.7}/tests/commands/test_logs.py +0 -0
- {devservices-1.0.5 → devservices-1.0.7}/tests/commands/test_status.py +0 -0
- {devservices-1.0.5 → devservices-1.0.7}/tests/commands/test_update.py +0 -0
- {devservices-1.0.5 → devservices-1.0.7}/tests/configs/test_service_config.py +0 -0
- {devservices-1.0.5 → devservices-1.0.7}/tests/conftest.py +0 -0
- {devservices-1.0.5 → devservices-1.0.7}/tests/utils/test_install_binary.py +0 -0
- {devservices-1.0.5 → devservices-1.0.7}/tests/utils/test_services.py +0 -0
- {devservices-1.0.5 → devservices-1.0.7}/tests/utils/test_state.py +0 -0
|
@@ -14,7 +14,6 @@ from devservices.constants import DEPENDENCY_CONFIG_VERSION
|
|
|
14
14
|
from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR
|
|
15
15
|
from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
|
|
16
16
|
from devservices.constants import DEVSERVICES_DIR_NAME
|
|
17
|
-
from devservices.constants import DOCKER_COMPOSE_COMMAND_LENGTH
|
|
18
17
|
from devservices.exceptions import ConfigError
|
|
19
18
|
from devservices.exceptions import DependencyError
|
|
20
19
|
from devservices.exceptions import DockerComposeError
|
|
@@ -24,6 +23,7 @@ from devservices.utils.console import Status
|
|
|
24
23
|
from devservices.utils.dependencies import get_non_shared_remote_dependencies
|
|
25
24
|
from devservices.utils.dependencies import install_and_verify_dependencies
|
|
26
25
|
from devservices.utils.dependencies import InstalledRemoteDependency
|
|
26
|
+
from devservices.utils.docker_compose import DockerComposeCommand
|
|
27
27
|
from devservices.utils.docker_compose import get_docker_compose_commands_to_run
|
|
28
28
|
from devservices.utils.docker_compose import run_cmd
|
|
29
29
|
from devservices.utils.services import find_matching_service
|
|
@@ -90,9 +90,14 @@ def down(args: Namespace) -> None:
|
|
|
90
90
|
capture_exception(de)
|
|
91
91
|
status.failure(str(de))
|
|
92
92
|
exit(1)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
try:
|
|
94
|
+
remote_dependencies = get_non_shared_remote_dependencies(
|
|
95
|
+
service, remote_dependencies
|
|
96
|
+
)
|
|
97
|
+
except DependencyError as de:
|
|
98
|
+
capture_exception(de)
|
|
99
|
+
status.failure(str(de))
|
|
100
|
+
exit(1)
|
|
96
101
|
try:
|
|
97
102
|
_down(service, remote_dependencies, list(mode_dependencies), status)
|
|
98
103
|
except DockerComposeError as dce:
|
|
@@ -106,12 +111,12 @@ def down(args: Namespace) -> None:
|
|
|
106
111
|
|
|
107
112
|
|
|
108
113
|
def _bring_down_dependency(
|
|
109
|
-
cmd:
|
|
114
|
+
cmd: DockerComposeCommand, current_env: dict[str, str], status: Status
|
|
110
115
|
) -> subprocess.CompletedProcess[str]:
|
|
111
116
|
# TODO: Get rid of these constants, we need a smarter way to determine the containers being brought down
|
|
112
|
-
for dependency in cmd
|
|
117
|
+
for dependency in cmd.services:
|
|
113
118
|
status.info(f"Stopping {dependency}")
|
|
114
|
-
return run_cmd(cmd, current_env)
|
|
119
|
+
return run_cmd(cmd.full_command, current_env)
|
|
115
120
|
|
|
116
121
|
|
|
117
122
|
def _down(
|
|
@@ -114,7 +114,7 @@ def _logs(
|
|
|
114
114
|
|
|
115
115
|
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
116
116
|
futures = [
|
|
117
|
-
executor.submit(run_cmd, cmd, current_env)
|
|
117
|
+
executor.submit(run_cmd, cmd.full_command, current_env)
|
|
118
118
|
for cmd in docker_compose_commands
|
|
119
119
|
]
|
|
120
120
|
for future in concurrent.futures.as_completed(futures):
|
|
@@ -0,0 +1,94 @@
|
|
|
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.constants import DEVSERVICES_ORCHESTRATOR_LABEL
|
|
11
|
+
from devservices.constants import DOCKER_NETWORK_NAME
|
|
12
|
+
from devservices.exceptions import DockerDaemonNotRunningError
|
|
13
|
+
from devservices.exceptions import DockerError
|
|
14
|
+
from devservices.utils.console import Console
|
|
15
|
+
from devservices.utils.console import Status
|
|
16
|
+
from devservices.utils.docker import get_matching_containers
|
|
17
|
+
from devservices.utils.docker import get_matching_networks
|
|
18
|
+
from devservices.utils.docker import get_volumes_for_containers
|
|
19
|
+
from devservices.utils.docker import remove_docker_resources
|
|
20
|
+
from devservices.utils.docker import stop_containers
|
|
21
|
+
from devservices.utils.state import State
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
25
|
+
parser = subparsers.add_parser("purge", help="Purge the local devservices cache")
|
|
26
|
+
parser.set_defaults(func=purge)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def purge(_args: Namespace) -> None:
|
|
30
|
+
"""Purge the local devservices state and cache and remove all devservices containers and volumes."""
|
|
31
|
+
console = Console()
|
|
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
|
+
|
|
42
|
+
try:
|
|
43
|
+
devservices_containers = get_matching_containers(DEVSERVICES_ORCHESTRATOR_LABEL)
|
|
44
|
+
except DockerDaemonNotRunningError as e:
|
|
45
|
+
console.warning(str(e))
|
|
46
|
+
return
|
|
47
|
+
except DockerError as de:
|
|
48
|
+
console.failure(f"Failed to get devservices containers {de.stderr}")
|
|
49
|
+
exit(1)
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
devservices_volumes = get_volumes_for_containers(devservices_containers)
|
|
53
|
+
except DockerError as e:
|
|
54
|
+
console.failure(f"Failed to get devservices volumes {e.stderr}")
|
|
55
|
+
exit(1)
|
|
56
|
+
|
|
57
|
+
with Status(
|
|
58
|
+
lambda: console.warning("Stopping all devservices containers"),
|
|
59
|
+
lambda: console.success("All devservices containers have been stopped"),
|
|
60
|
+
):
|
|
61
|
+
try:
|
|
62
|
+
stop_containers(devservices_containers, should_remove=True)
|
|
63
|
+
except DockerError as e:
|
|
64
|
+
console.failure(f"Failed to stop devservices containers {e.stderr}")
|
|
65
|
+
exit(1)
|
|
66
|
+
|
|
67
|
+
console.warning("Removing any devservices docker volumes")
|
|
68
|
+
if len(devservices_volumes) == 0:
|
|
69
|
+
console.success("No devservices volumes found to remove")
|
|
70
|
+
else:
|
|
71
|
+
try:
|
|
72
|
+
remove_docker_resources("volume", list(devservices_volumes))
|
|
73
|
+
console.success("All devservices volumes removed")
|
|
74
|
+
except DockerError as e:
|
|
75
|
+
# We don't want to exit here since we still want to try to remove the networks
|
|
76
|
+
console.failure(f"Failed to remove devservices volumes {e.stderr}")
|
|
77
|
+
|
|
78
|
+
console.warning("Removing any devservices networks")
|
|
79
|
+
try:
|
|
80
|
+
devservices_networks = get_matching_networks(DOCKER_NETWORK_NAME)
|
|
81
|
+
except DockerError as e:
|
|
82
|
+
console.failure(f"Failed to get devservices networks {e.stderr}")
|
|
83
|
+
exit(1)
|
|
84
|
+
if len(devservices_networks) == 0:
|
|
85
|
+
console.success("No devservices networks found to remove")
|
|
86
|
+
else:
|
|
87
|
+
try:
|
|
88
|
+
remove_docker_resources("network", devservices_networks)
|
|
89
|
+
console.success("All devservices networks removed")
|
|
90
|
+
except DockerError as e:
|
|
91
|
+
console.failure(f"Failed to remove devservices networks {e.stderr}")
|
|
92
|
+
exit(1)
|
|
93
|
+
|
|
94
|
+
console.success("The local devservices cache and state has been purged")
|
|
@@ -152,7 +152,7 @@ def _status(
|
|
|
152
152
|
|
|
153
153
|
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
154
154
|
futures = [
|
|
155
|
-
executor.submit(run_cmd, cmd, current_env)
|
|
155
|
+
executor.submit(run_cmd, cmd.full_command, current_env)
|
|
156
156
|
for cmd in docker_compose_commands
|
|
157
157
|
]
|
|
158
158
|
for future in concurrent.futures.as_completed(futures):
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import concurrent.futures
|
|
3
4
|
import os
|
|
4
5
|
import subprocess
|
|
5
6
|
from argparse import _SubParsersAction
|
|
@@ -13,8 +14,8 @@ from devservices.constants import DEPENDENCY_CONFIG_VERSION
|
|
|
13
14
|
from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR
|
|
14
15
|
from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
|
|
15
16
|
from devservices.constants import DEVSERVICES_DIR_NAME
|
|
16
|
-
from devservices.constants import DOCKER_COMPOSE_COMMAND_LENGTH
|
|
17
17
|
from devservices.exceptions import ConfigError
|
|
18
|
+
from devservices.exceptions import ContainerHealthcheckFailedError
|
|
18
19
|
from devservices.exceptions import DependencyError
|
|
19
20
|
from devservices.exceptions import DockerComposeError
|
|
20
21
|
from devservices.exceptions import ModeDoesNotExistError
|
|
@@ -24,6 +25,9 @@ from devservices.utils.console import Status
|
|
|
24
25
|
from devservices.utils.dependencies import construct_dependency_graph
|
|
25
26
|
from devservices.utils.dependencies import install_and_verify_dependencies
|
|
26
27
|
from devservices.utils.dependencies import InstalledRemoteDependency
|
|
28
|
+
from devservices.utils.docker import check_all_containers_healthy
|
|
29
|
+
from devservices.utils.docker_compose import DockerComposeCommand
|
|
30
|
+
from devservices.utils.docker_compose import get_container_names_for_project
|
|
27
31
|
from devservices.utils.docker_compose import get_docker_compose_commands_to_run
|
|
28
32
|
from devservices.utils.docker_compose import run_cmd
|
|
29
33
|
from devservices.utils.services import find_matching_service
|
|
@@ -101,12 +105,12 @@ def up(args: Namespace) -> None:
|
|
|
101
105
|
|
|
102
106
|
|
|
103
107
|
def _bring_up_dependency(
|
|
104
|
-
cmd:
|
|
108
|
+
cmd: DockerComposeCommand, current_env: dict[str, str], status: Status
|
|
105
109
|
) -> subprocess.CompletedProcess[str]:
|
|
106
110
|
# TODO: Get rid of these constants, we need a smarter way to determine the containers being brought up
|
|
107
|
-
for dependency in cmd
|
|
111
|
+
for dependency in cmd.services:
|
|
108
112
|
status.info(f"Starting {dependency}")
|
|
109
|
-
return run_cmd(cmd, current_env)
|
|
113
|
+
return run_cmd(cmd.full_command, current_env)
|
|
110
114
|
|
|
111
115
|
|
|
112
116
|
def _up(
|
|
@@ -128,7 +132,7 @@ def _up(
|
|
|
128
132
|
current_env[
|
|
129
133
|
DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
|
|
130
134
|
] = relative_local_dependency_directory
|
|
131
|
-
options = ["-d"]
|
|
135
|
+
options = ["-d", "--pull", "always"]
|
|
132
136
|
dependency_graph = construct_dependency_graph(service, modes=modes)
|
|
133
137
|
starting_order = dependency_graph.get_starting_order()
|
|
134
138
|
sorted_remote_dependencies = sorted(
|
|
@@ -144,8 +148,31 @@ def _up(
|
|
|
144
148
|
mode_dependencies=mode_dependencies,
|
|
145
149
|
)
|
|
146
150
|
|
|
151
|
+
containers_to_check = []
|
|
152
|
+
with concurrent.futures.ThreadPoolExecutor() as dependency_executor:
|
|
153
|
+
futures = [
|
|
154
|
+
dependency_executor.submit(_bring_up_dependency, cmd, current_env, status)
|
|
155
|
+
for cmd in docker_compose_commands
|
|
156
|
+
]
|
|
157
|
+
for future in concurrent.futures.as_completed(futures):
|
|
158
|
+
_ = future.result()
|
|
159
|
+
|
|
147
160
|
for cmd in docker_compose_commands:
|
|
148
|
-
|
|
161
|
+
try:
|
|
162
|
+
container_names = get_container_names_for_project(
|
|
163
|
+
cmd.project_name, cmd.config_path
|
|
164
|
+
)
|
|
165
|
+
containers_to_check.extend(container_names)
|
|
166
|
+
except DockerComposeError as dce:
|
|
167
|
+
status.failure(
|
|
168
|
+
f"Failed to get containers to healthcheck for {cmd.project_name}: {dce.stderr}"
|
|
169
|
+
)
|
|
170
|
+
exit(1)
|
|
171
|
+
try:
|
|
172
|
+
check_all_containers_healthy(status, containers_to_check)
|
|
173
|
+
except ContainerHealthcheckFailedError as e:
|
|
174
|
+
status.failure(str(e))
|
|
175
|
+
exit(1)
|
|
149
176
|
|
|
150
177
|
|
|
151
178
|
def _create_devservices_network() -> None:
|
|
@@ -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,7 +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")
|
|
15
|
-
|
|
16
|
+
DEVSERVICES_ORCHESTRATOR_LABEL = "orchestrator=devservices"
|
|
16
17
|
|
|
17
18
|
DEPENDENCY_CONFIG_VERSION = "v1"
|
|
18
19
|
DEPENDENCY_GIT_PARTIAL_CLONE_CONFIG_OPTIONS = {
|
|
@@ -21,9 +22,20 @@ DEPENDENCY_GIT_PARTIAL_CLONE_CONFIG_OPTIONS = {
|
|
|
21
22
|
"core.sparseCheckout": "true",
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
DEVSERVICES_RELEASES_URL = (
|
|
26
|
+
"https://api.github.com/repos/getsentry/devservices/releases/latest"
|
|
27
|
+
)
|
|
24
28
|
DOCKER_COMPOSE_DOWNLOAD_URL = "https://github.com/docker/compose/releases/download"
|
|
25
29
|
DEVSERVICES_DOWNLOAD_URL = "https://github.com/getsentry/devservices/releases/download"
|
|
26
30
|
BINARY_PERMISSIONS = 0o755
|
|
27
31
|
MAX_LOG_LINES = "100"
|
|
28
32
|
LOGGER_NAME = "devservices"
|
|
29
33
|
DOCKER_NETWORK_NAME = "devservices"
|
|
34
|
+
|
|
35
|
+
# Latest Version Cache
|
|
36
|
+
DEVSERVICES_LATEST_VERSION_CACHE_FILE = os.path.join(
|
|
37
|
+
DEVSERVICES_CACHE_DIR, "latest_version.txt"
|
|
38
|
+
)
|
|
39
|
+
DEVSERVICES_LATEST_VERSION_CACHE_TTL = timedelta(minutes=15)
|
|
40
|
+
HEALTHCHECK_TIMEOUT = 30
|
|
41
|
+
HEALTHCHECK_INTERVAL = 5
|
|
@@ -57,8 +57,8 @@ class DockerComposeInstallationError(BinaryInstallError):
|
|
|
57
57
|
pass
|
|
58
58
|
|
|
59
59
|
|
|
60
|
-
class
|
|
61
|
-
"""Base class for Docker
|
|
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
|
|
|
@@ -121,3 +127,14 @@ class FailedToSetGitConfigError(GitConfigError):
|
|
|
121
127
|
"""Raised when a git config cannot be set."""
|
|
122
128
|
|
|
123
129
|
pass
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class ContainerHealthcheckFailedError(Exception):
|
|
133
|
+
"""Raised when a container is not healthy."""
|
|
134
|
+
|
|
135
|
+
def __init__(self, container_name: str, timeout: int):
|
|
136
|
+
self.container_name = container_name
|
|
137
|
+
self.timeout = timeout
|
|
138
|
+
|
|
139
|
+
def __str__(self) -> str:
|
|
140
|
+
return f"Container {self.container_name} did not become healthy within {self.timeout} seconds."
|
|
@@ -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
|
|
@@ -12,6 +12,8 @@ from dataclasses import dataclass
|
|
|
12
12
|
from typing import TextIO
|
|
13
13
|
from typing import TypeGuard
|
|
14
14
|
|
|
15
|
+
from sentry_sdk import set_context
|
|
16
|
+
|
|
15
17
|
from devservices.configs.service_config import Dependency
|
|
16
18
|
from devservices.configs.service_config import load_service_config_from_file
|
|
17
19
|
from devservices.configs.service_config import RemoteConfig
|
|
@@ -36,6 +38,17 @@ from devservices.utils.services import find_matching_service
|
|
|
36
38
|
from devservices.utils.services import Service
|
|
37
39
|
from devservices.utils.state import State
|
|
38
40
|
|
|
41
|
+
RELEVANT_GIT_CONFIG_KEYS = [
|
|
42
|
+
"init.defaultbranch",
|
|
43
|
+
"core.sparsecheckout",
|
|
44
|
+
"remote.origin.url",
|
|
45
|
+
"remote.origin.fetch",
|
|
46
|
+
"remote.origin.promisor",
|
|
47
|
+
"remote.origin.partialclonefilter",
|
|
48
|
+
"protocol.version",
|
|
49
|
+
"extensions.partialclone",
|
|
50
|
+
]
|
|
51
|
+
|
|
39
52
|
|
|
40
53
|
class DependencyGraph:
|
|
41
54
|
def __init__(self) -> None:
|
|
@@ -149,6 +162,28 @@ class GitConfigManager:
|
|
|
149
162
|
if self.sparse_pattern:
|
|
150
163
|
self.sparse_checkout_manager.set_sparse_checkout(self.sparse_pattern)
|
|
151
164
|
|
|
165
|
+
def get_relevant_config(self) -> dict[str, str]:
|
|
166
|
+
"""
|
|
167
|
+
Get the relevant git config entries (to avoid logging sensitive information)
|
|
168
|
+
"""
|
|
169
|
+
git_config = (
|
|
170
|
+
subprocess.check_output(
|
|
171
|
+
["git", "config", "--list"],
|
|
172
|
+
cwd=self.repo_dir,
|
|
173
|
+
stderr=subprocess.PIPE,
|
|
174
|
+
)
|
|
175
|
+
.decode()
|
|
176
|
+
.strip()
|
|
177
|
+
)
|
|
178
|
+
git_config_dict = dict()
|
|
179
|
+
for line in git_config.split("\n"):
|
|
180
|
+
if not line:
|
|
181
|
+
continue
|
|
182
|
+
key, value = line.split("=")
|
|
183
|
+
if key in RELEVANT_GIT_CONFIG_KEYS:
|
|
184
|
+
git_config_dict[key] = value
|
|
185
|
+
return git_config_dict
|
|
186
|
+
|
|
152
187
|
def _set_config(self, key: str, value: str) -> None:
|
|
153
188
|
"""
|
|
154
189
|
Set a git config option for the repo
|
|
@@ -411,12 +446,15 @@ def _update_dependency(
|
|
|
411
446
|
repo_link=dependency.repo_link,
|
|
412
447
|
branch=dependency.branch,
|
|
413
448
|
) from e
|
|
449
|
+
|
|
414
450
|
try:
|
|
415
451
|
_run_command(
|
|
416
452
|
["git", "fetch", "origin", dependency.branch, "--filter=blob:none"],
|
|
417
453
|
cwd=dependency_repo_dir,
|
|
418
454
|
)
|
|
419
455
|
except subprocess.CalledProcessError as e:
|
|
456
|
+
# Try to set the git config context to help with debugging
|
|
457
|
+
_try_set_git_config_context(git_config_manager)
|
|
420
458
|
raise DependencyError(
|
|
421
459
|
repo_name=dependency.repo_name,
|
|
422
460
|
repo_link=dependency.repo_link,
|
|
@@ -536,6 +574,17 @@ def _run_command(
|
|
|
536
574
|
subprocess.run(cmd, cwd=cwd, check=True, stdout=stdout, stderr=subprocess.DEVNULL)
|
|
537
575
|
|
|
538
576
|
|
|
577
|
+
def _try_set_git_config_context(
|
|
578
|
+
git_config_manager: GitConfigManager,
|
|
579
|
+
) -> None:
|
|
580
|
+
try:
|
|
581
|
+
git_config = git_config_manager.get_relevant_config()
|
|
582
|
+
set_context("git_config", git_config)
|
|
583
|
+
except subprocess.CalledProcessError as e:
|
|
584
|
+
logger = logging.getLogger(LOGGER_NAME)
|
|
585
|
+
logger.exception(e)
|
|
586
|
+
|
|
587
|
+
|
|
539
588
|
def get_remote_dependency_config(remote_config: RemoteConfig) -> ServiceConfig:
|
|
540
589
|
dependency_repo_dir = os.path.join(
|
|
541
590
|
DEVSERVICES_DEPENDENCIES_CACHE_DIR,
|