devservices 1.3.0__tar.gz → 1.3.1__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.3.0 → devservices-1.3.1}/PKG-INFO +1 -1
- {devservices-1.3.0 → devservices-1.3.1}/README.md +3 -2
- {devservices-1.3.0 → devservices-1.3.1}/devservices/commands/up.py +3 -1
- {devservices-1.3.0 → devservices-1.3.1}/devservices/configs/service_config.py +13 -0
- {devservices-1.3.0 → devservices-1.3.1}/devservices/constants.py +2 -2
- {devservices-1.3.0 → devservices-1.3.1}/devservices/utils/docker.py +14 -5
- {devservices-1.3.0 → devservices-1.3.1}/devservices.egg-info/PKG-INFO +1 -1
- {devservices-1.3.0 → devservices-1.3.1}/pyproject.toml +5 -3
- {devservices-1.3.0 → devservices-1.3.1}/tests/commands/test_up.py +2 -1
- {devservices-1.3.0 → devservices-1.3.1}/tests/configs/test_service_config.py +41 -0
- {devservices-1.3.0 → devservices-1.3.1}/tests/utils/test_docker.py +4 -0
- {devservices-1.3.0 → devservices-1.3.1}/LICENSE.md +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/devservices/__init__.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/devservices/commands/__init__.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/devservices/commands/down.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/devservices/commands/foreground.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/devservices/commands/list_dependencies.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/devservices/commands/list_services.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/devservices/commands/logs.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/devservices/commands/purge.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/devservices/commands/reset.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/devservices/commands/serve.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/devservices/commands/status.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/devservices/commands/toggle.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/devservices/commands/update.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/devservices/exceptions.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/devservices/main.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/devservices/utils/__init__.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/devservices/utils/check_for_update.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/devservices/utils/console.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/devservices/utils/dependencies.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/devservices/utils/devenv.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/devservices/utils/docker_compose.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/devservices/utils/file_lock.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/devservices/utils/git.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/devservices/utils/install_binary.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/devservices/utils/services.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/devservices/utils/state.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/devservices/utils/supervisor.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/devservices.egg-info/SOURCES.txt +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/devservices.egg-info/dependency_links.txt +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/devservices.egg-info/entry_points.txt +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/devservices.egg-info/requires.txt +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/devservices.egg-info/top_level.txt +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/setup.cfg +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/testing/__init__.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/testing/utils.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/tests/__init__.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/tests/commands/test_down.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/tests/commands/test_foreground.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/tests/commands/test_list_dependencies.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/tests/commands/test_list_services.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/tests/commands/test_logs.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/tests/commands/test_purge.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/tests/commands/test_reset.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/tests/commands/test_serve.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/tests/commands/test_status.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/tests/commands/test_toggle.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/tests/commands/test_update.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/tests/conftest.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/tests/utils/test_check_for_update.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/tests/utils/test_dependencies.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/tests/utils/test_docker_compose.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/tests/utils/test_git.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/tests/utils/test_install_binary.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/tests/utils/test_services.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/tests/utils/test_state.py +0 -0
- {devservices-1.3.0 → devservices-1.3.1}/tests/utils/test_supervisor.py +0 -0
|
@@ -33,7 +33,7 @@ NOTE: service-name is an optional parameter. If not provided, devservices will a
|
|
|
33
33
|
The recommended way to install devservices is through a virtualenv in the requirements.txt. Once that is installed and a devservices config file is added, you should be able to run `devservices up` to begin local development.
|
|
34
34
|
|
|
35
35
|
```
|
|
36
|
-
devservices==1.3.
|
|
36
|
+
devservices==1.3.1
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
### 2. Add devservices config files
|
|
@@ -50,6 +50,7 @@ The configuration file is a yaml file that looks like this:
|
|
|
50
50
|
# - local: A dependency that is defined in the config file. These dependencies do not have a remote field and must correspond to either a service defined in the 'services' section or a program defined in the 'x-programs' section.
|
|
51
51
|
# - remote: A dependency that is defined in the devservices directory in a remote repository. These configs are automatically fetched from the remote repository and installed. Any dependency with a remote field will be treated as a remote dependency. Example: https://github.com/getsentry/snuba/blob/59a5258ccbb502827ebc1d3b1bf80c607a3301bf/devservices/config.yml#L8
|
|
52
52
|
# - modes: A list of modes for the service. Each mode includes a list of dependencies that are used in that mode.
|
|
53
|
+
# - healthcheck_timeout: Optional. The number of seconds to wait for all containers to become healthy before failing. Defaults to 180.
|
|
53
54
|
x-sentry-service-config:
|
|
54
55
|
version: 0.1
|
|
55
56
|
service_name: example-service
|
|
@@ -173,5 +174,5 @@ networks:
|
|
|
173
174
|
```sh
|
|
174
175
|
uv sync
|
|
175
176
|
direnv allow
|
|
176
|
-
pytest
|
|
177
|
+
pytest -n4
|
|
177
178
|
```
|
|
@@ -390,7 +390,9 @@ def bring_up_docker_compose_services(
|
|
|
390
390
|
)
|
|
391
391
|
exit(1)
|
|
392
392
|
try:
|
|
393
|
-
check_all_containers_healthy(
|
|
393
|
+
check_all_containers_healthy(
|
|
394
|
+
status, containers_to_check, timeout=service.config.healthcheck_timeout
|
|
395
|
+
)
|
|
394
396
|
except ContainerHealthcheckFailedError as e:
|
|
395
397
|
status.failure(str(e))
|
|
396
398
|
exit(1)
|
|
@@ -9,6 +9,7 @@ from supervisor.options import ServerOptions
|
|
|
9
9
|
|
|
10
10
|
from devservices.constants import CONFIG_FILE_NAME
|
|
11
11
|
from devservices.constants import DEVSERVICES_DIR_NAME
|
|
12
|
+
from devservices.constants import HEALTHCHECK_TIMEOUT
|
|
12
13
|
from devservices.constants import DependencyType
|
|
13
14
|
from devservices.exceptions import ConfigNotFoundError
|
|
14
15
|
from devservices.exceptions import ConfigParseError
|
|
@@ -40,6 +41,7 @@ class ServiceConfig:
|
|
|
40
41
|
service_name: str
|
|
41
42
|
dependencies: dict[str, Dependency]
|
|
42
43
|
modes: dict[str, list[str]]
|
|
44
|
+
healthcheck_timeout: int = HEALTHCHECK_TIMEOUT
|
|
43
45
|
|
|
44
46
|
def __post_init__(self) -> None:
|
|
45
47
|
self._validate()
|
|
@@ -59,6 +61,14 @@ class ServiceConfig:
|
|
|
59
61
|
if "default" not in self.modes:
|
|
60
62
|
raise ConfigValidationError("Default mode is required in service config")
|
|
61
63
|
|
|
64
|
+
if isinstance(self.healthcheck_timeout, bool) or (
|
|
65
|
+
not isinstance(self.healthcheck_timeout, int)
|
|
66
|
+
or self.healthcheck_timeout <= 0
|
|
67
|
+
):
|
|
68
|
+
raise ConfigValidationError(
|
|
69
|
+
"healthcheck_timeout must be a positive integer"
|
|
70
|
+
)
|
|
71
|
+
|
|
62
72
|
for mode, services in self.modes.items():
|
|
63
73
|
if not isinstance(services, list):
|
|
64
74
|
raise ConfigValidationError(f"Services in mode '{mode}' must be a list")
|
|
@@ -142,6 +152,9 @@ def load_service_config_from_file(
|
|
|
142
152
|
service_name=service_config_data.get("service_name"),
|
|
143
153
|
dependencies=dependencies,
|
|
144
154
|
modes=service_config_data.get("modes", {}),
|
|
155
|
+
healthcheck_timeout=service_config_data.get("healthcheck_timeout")
|
|
156
|
+
if service_config_data.get("healthcheck_timeout") is not None
|
|
157
|
+
else HEALTHCHECK_TIMEOUT,
|
|
145
158
|
)
|
|
146
159
|
|
|
147
160
|
return service_config
|
|
@@ -65,7 +65,7 @@ DEVSERVICES_LATEST_VERSION_CACHE_FILE = os.path.join(
|
|
|
65
65
|
DEVSERVICES_CACHE_DIR, "latest_version.txt"
|
|
66
66
|
)
|
|
67
67
|
DEVSERVICES_LATEST_VERSION_CACHE_TTL = timedelta(minutes=15)
|
|
68
|
-
# Healthcheck timeout set to
|
|
69
|
-
HEALTHCHECK_TIMEOUT =
|
|
68
|
+
# Healthcheck timeout set to 3 minutes to account for slow healthchecks
|
|
69
|
+
HEALTHCHECK_TIMEOUT = 180
|
|
70
70
|
HEALTHCHECK_INTERVAL = 5
|
|
71
71
|
SUPERVISOR_TIMEOUT = 10
|
|
@@ -43,25 +43,34 @@ def check_docker_daemon_running() -> None:
|
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
def check_all_containers_healthy(
|
|
46
|
-
status: Status,
|
|
46
|
+
status: Status,
|
|
47
|
+
containers: list[ContainerNames],
|
|
48
|
+
timeout: int = HEALTHCHECK_TIMEOUT,
|
|
47
49
|
) -> None:
|
|
48
50
|
"""Ensures all containers are healthy."""
|
|
49
51
|
status.info("Waiting for all containers to be healthy")
|
|
50
52
|
with concurrent.futures.ThreadPoolExecutor() as healthcheck_executor:
|
|
51
53
|
futures = [
|
|
52
|
-
healthcheck_executor.submit(wait_for_healthy, container, status)
|
|
54
|
+
healthcheck_executor.submit(wait_for_healthy, container, status, timeout)
|
|
53
55
|
for container in containers
|
|
54
56
|
]
|
|
55
57
|
for future in concurrent.futures.as_completed(futures):
|
|
56
58
|
future.result()
|
|
57
59
|
|
|
58
60
|
|
|
59
|
-
def wait_for_healthy(
|
|
61
|
+
def wait_for_healthy(
|
|
62
|
+
container: ContainerNames,
|
|
63
|
+
status: Status,
|
|
64
|
+
timeout: int = HEALTHCHECK_TIMEOUT,
|
|
65
|
+
) -> None:
|
|
60
66
|
"""
|
|
61
67
|
Polls a Docker container's health status until it becomes healthy or a timeout is reached.
|
|
62
68
|
"""
|
|
69
|
+
status.info(
|
|
70
|
+
f"Waiting for {container.short_name} to be healthy (timeout: {timeout}s)"
|
|
71
|
+
)
|
|
63
72
|
start = time.time()
|
|
64
|
-
while time.time() - start <
|
|
73
|
+
while time.time() - start < timeout:
|
|
65
74
|
# Run docker inspect to get the container's health status
|
|
66
75
|
try:
|
|
67
76
|
# For containers with no healthchecks, the output will be "unknown"
|
|
@@ -96,7 +105,7 @@ def wait_for_healthy(container: ContainerNames, status: Status) -> None:
|
|
|
96
105
|
# If not healthy, wait and try again
|
|
97
106
|
time.sleep(HEALTHCHECK_INTERVAL)
|
|
98
107
|
|
|
99
|
-
raise ContainerHealthcheckFailedError(container.short_name,
|
|
108
|
+
raise ContainerHealthcheckFailedError(container.short_name, timeout)
|
|
100
109
|
|
|
101
110
|
|
|
102
111
|
@dataclass
|
|
@@ -4,8 +4,9 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "devservices"
|
|
7
|
-
version = "1.3.
|
|
7
|
+
version = "1.3.1"
|
|
8
8
|
# 3.11 is just for internal pypi compat
|
|
9
|
+
# but we test/support on 3.13
|
|
9
10
|
requires-python = ">=3.11"
|
|
10
11
|
dependencies = [
|
|
11
12
|
"pyyaml",
|
|
@@ -21,13 +22,14 @@ dev = [
|
|
|
21
22
|
"freezegun",
|
|
22
23
|
"mypy",
|
|
23
24
|
"pre-commit",
|
|
24
|
-
"pytest",
|
|
25
|
+
"pytest>9",
|
|
25
26
|
"pytest-cov",
|
|
26
27
|
"ruff",
|
|
27
28
|
"setuptools>=70",
|
|
28
29
|
"shellcheck-py",
|
|
29
30
|
"wheel",
|
|
30
31
|
"types-PyYAML",
|
|
32
|
+
"pytest-xdist>=3.5.0",
|
|
31
33
|
]
|
|
32
34
|
|
|
33
35
|
[project.scripts]
|
|
@@ -40,7 +42,7 @@ find = {}
|
|
|
40
42
|
url = "https://pypi.devinfra.sentry.io/simple"
|
|
41
43
|
|
|
42
44
|
[tool.mypy]
|
|
43
|
-
python_version = "3.
|
|
45
|
+
python_version = "3.13"
|
|
44
46
|
strict = true
|
|
45
47
|
ignore_missing_imports = true
|
|
46
48
|
|
|
@@ -865,7 +865,7 @@ def test_up_docker_compose_container_healthcheck_failed(
|
|
|
865
865
|
assert "Starting clickhouse" in captured.out.strip()
|
|
866
866
|
assert "Starting redis" in captured.out.strip()
|
|
867
867
|
assert (
|
|
868
|
-
"Container container1 did not become healthy within
|
|
868
|
+
"Container container1 did not become healthy within 180 seconds."
|
|
869
869
|
in captured.out.strip()
|
|
870
870
|
)
|
|
871
871
|
|
|
@@ -1376,6 +1376,7 @@ def test_up_multiple_modes_overlapping_running_service(
|
|
|
1376
1376
|
mock_check_all_containers_healthy.assert_called_once_with(
|
|
1377
1377
|
mock.ANY,
|
|
1378
1378
|
["container1", "container2"],
|
|
1379
|
+
timeout=mock.ANY,
|
|
1379
1380
|
)
|
|
1380
1381
|
|
|
1381
1382
|
captured = capsys.readouterr()
|
|
@@ -111,6 +111,7 @@ def test_load_service_config_from_file(
|
|
|
111
111
|
for key, value in dependencies.items()
|
|
112
112
|
},
|
|
113
113
|
"modes": modes,
|
|
114
|
+
"healthcheck_timeout": 180,
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
|
|
@@ -131,9 +132,49 @@ def test_load_service_config_from_file_no_dependencies(tmp_path: Path) -> None:
|
|
|
131
132
|
"service_name": "example-service",
|
|
132
133
|
"dependencies": {},
|
|
133
134
|
"modes": {"default": []},
|
|
135
|
+
"healthcheck_timeout": 180,
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
|
|
139
|
+
def test_load_service_config_from_file_null_healthcheck_timeout(
|
|
140
|
+
tmp_path: Path,
|
|
141
|
+
) -> None:
|
|
142
|
+
config = {
|
|
143
|
+
"x-sentry-service-config": {
|
|
144
|
+
"version": 0.1,
|
|
145
|
+
"service_name": "example-service",
|
|
146
|
+
"modes": {"default": []},
|
|
147
|
+
"healthcheck_timeout": None,
|
|
148
|
+
},
|
|
149
|
+
"services": {},
|
|
150
|
+
}
|
|
151
|
+
create_config_file(tmp_path, config)
|
|
152
|
+
|
|
153
|
+
service_config = load_service_config_from_file(str(tmp_path))
|
|
154
|
+
assert service_config.healthcheck_timeout == 180
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@pytest.mark.parametrize("invalid_timeout", [-1, 0, "120", True, False])
|
|
158
|
+
def test_load_service_config_from_file_invalid_healthcheck_timeout(
|
|
159
|
+
tmp_path: Path,
|
|
160
|
+
invalid_timeout: object,
|
|
161
|
+
) -> None:
|
|
162
|
+
config = {
|
|
163
|
+
"x-sentry-service-config": {
|
|
164
|
+
"version": 0.1,
|
|
165
|
+
"service_name": "example-service",
|
|
166
|
+
"modes": {"default": []},
|
|
167
|
+
"healthcheck_timeout": invalid_timeout,
|
|
168
|
+
},
|
|
169
|
+
"services": {},
|
|
170
|
+
}
|
|
171
|
+
create_config_file(tmp_path, config)
|
|
172
|
+
|
|
173
|
+
with pytest.raises(ConfigValidationError) as e:
|
|
174
|
+
load_service_config_from_file(str(tmp_path))
|
|
175
|
+
assert str(e.value) == "healthcheck_timeout must be a positive integer"
|
|
176
|
+
|
|
177
|
+
|
|
137
178
|
def test_load_service_config_from_file_missing_config(tmp_path: Path) -> None:
|
|
138
179
|
with pytest.raises(ConfigNotFoundError) as e:
|
|
139
180
|
load_service_config_from_file(str(tmp_path))
|
|
@@ -533,10 +533,12 @@ def test_check_all_containers_healthy_success(
|
|
|
533
533
|
mock.call(
|
|
534
534
|
ContainerNames(name="devservices-container1", short_name="container1"),
|
|
535
535
|
mock_status,
|
|
536
|
+
180,
|
|
536
537
|
),
|
|
537
538
|
mock.call(
|
|
538
539
|
ContainerNames(name="devservices-container2", short_name="container2"),
|
|
539
540
|
mock_status,
|
|
541
|
+
180,
|
|
540
542
|
),
|
|
541
543
|
]
|
|
542
544
|
)
|
|
@@ -671,10 +673,12 @@ def test_check_all_containers_healthy_failure(
|
|
|
671
673
|
mock.call(
|
|
672
674
|
ContainerNames(name="devservices-container1", short_name="container1"),
|
|
673
675
|
mock_status,
|
|
676
|
+
180,
|
|
674
677
|
),
|
|
675
678
|
mock.call(
|
|
676
679
|
ContainerNames(name="devservices-container2", short_name="container2"),
|
|
677
680
|
mock_status,
|
|
681
|
+
180,
|
|
678
682
|
),
|
|
679
683
|
]
|
|
680
684
|
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|