Skip to content

Commit e35a6c4

Browse files
authored
Merge pull request #1062 from GitGuardian/severine/dont-block-push-on-server-503
feat: do not block pre-receive when server isn't responding (503)
2 parents ad53be3 + 9e9cec1 commit e35a6c4

File tree

7 files changed

+84
-6
lines changed

7 files changed

+84
-6
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
### Changed
2+
3+
- Pre-receive hook isn't blocking anymore when GitGuardian server is temporarily unavailable (return 5xx status code).

ggshield/cmd/secret/scan/prereceive.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from ggshield.core.cache import ReadOnlyCache
1818
from ggshield.core.client import create_client_from_config
1919
from ggshield.core.config import Config
20-
from ggshield.core.errors import handle_exception
20+
from ggshield.core.errors import ExitCode, handle_exception
2121
from ggshield.core.git_hooks.prereceive import (
2222
get_breakglass_option,
2323
get_prereceive_timeout,
@@ -137,5 +137,8 @@ def prereceive_cmd(
137137
ui.display_error("\nPre-receive hook took too long")
138138
process.kill()
139139
return 0
140+
if process.exitcode == ExitCode.GITGUARDIAN_SERVER_UNAVAILABLE:
141+
ui.display_error("\nGitGuardian server is not responding. Skipping checks.")
142+
return 0
140143

141144
return process.exitcode

ggshield/core/client.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@
88

99
from .config import Config
1010
from .constants import DEFAULT_INSTANCE_URL
11-
from .errors import APIKeyCheckError, UnexpectedError, UnknownInstanceError
11+
from .errors import (
12+
APIKeyCheckError,
13+
ServiceUnavailableError,
14+
UnexpectedError,
15+
UnknownInstanceError,
16+
)
1217
from .ui.client_callbacks import ClientCallbacks
1318

1419

@@ -103,9 +108,13 @@ def check_client_api_key(client: GGClient) -> None:
103108
raise APIKeyCheckError(client.base_uri, "Invalid API key.")
104109
elif response.status_code == 404:
105110
raise UnexpectedError(
106-
"The server returned a 404 error. Check your instance URL" " settings."
111+
"The server returned a 404 error. Check your instance URL settings.",
112+
)
113+
elif response.status_code is not None and 500 <= response.status_code < 600:
114+
raise ServiceUnavailableError(
115+
message=f"GitGuardian server is not responding.\nDetails: {response.detail}",
107116
)
108117
else:
109118
raise UnexpectedError(
110-
f"Server is not responding as expected.\nDetails: {response.detail}"
119+
f"GitGuardian server is not responding as expected.\nDetails: {response.detail}"
111120
)

ggshield/core/errors.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ class ExitCode(IntEnum):
3333
USAGE_ERROR = 2
3434
# auth subcommand failed
3535
AUTHENTICATION_ERROR = 3
36+
# GitGuardian server is not responding
37+
GITGUARDIAN_SERVER_UNAVAILABLE = 4
3638

3739
# Add new exit codes here.
3840
# If you add a new exit code, make sure you also add it to the documentation.
@@ -133,6 +135,15 @@ def __init__(self, instance: str, message: str):
133135
super().__init__(instance, message)
134136

135137

138+
class ServiceUnavailableError(_ExitError):
139+
"""
140+
Raised when the server is unavailable
141+
"""
142+
143+
def __init__(self, message: str):
144+
super().__init__(ExitCode.GITGUARDIAN_SERVER_UNAVAILABLE, message)
145+
146+
136147
def format_validation_error(exc: ValidationError) -> str:
137148
"""
138149
Take a Marshmallow ValidationError and turn it into a more user-friendly message

tests/unit/cmd/scan/test_prereceive.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import pytest
44
from click.testing import CliRunner
5+
from pygitguardian.models import Detail
56

67
from ggshield.__main__ import cli
78
from ggshield.core.config.user_config import SecretConfig
@@ -363,3 +364,27 @@ def test_stdin_input_deletion(
363364
)
364365
assert_invoke_ok(result)
365366
assert "Deletion event or nothing to scan.\n" in result.output
367+
368+
@patch(
369+
"pygitguardian.client.GGClient.read_metadata",
370+
return_value=Detail("Service is unavailable", 503),
371+
)
372+
def test_server_unavailable(self, _: Mock, tmp_path, cli_fs_runner: CliRunner):
373+
"""
374+
GIVEN a repo on which the command is ran
375+
WHEN the server is not responding (503)
376+
THEN it should return 0
377+
AND display an error message
378+
"""
379+
# setting up repo to run the command
380+
repo = create_pre_receive_repo(tmp_path)
381+
old_sha = repo.get_top_sha()
382+
shas = [repo.create_commit() for _ in range(3)]
383+
with cd(repo.path):
384+
result = cli_fs_runner.invoke(
385+
cli,
386+
["-v", "secret", "scan", "pre-receive"],
387+
input=f"{old_sha} {shas[-1]} origin/main\n",
388+
)
389+
assert_invoke_ok(result)
390+
assert "GitGuardian server is not responding" in result.output

tests/unit/cmd/scan/test_repo.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
from unittest.mock import Mock, patch
2+
3+
from pygitguardian.models import Detail
4+
15
from ggshield.__main__ import cli
26
from ggshield.core.errors import ExitCode
37
from tests.unit.conftest import assert_invoke_exited_with
@@ -35,3 +39,20 @@ def test_invalid_scan_repo_url(self, cli_fs_runner):
3539
"Error: trial.gitguardian.com/gitguardian/ggshield is"
3640
" neither a valid path nor a git URL" in result.output
3741
)
42+
43+
@patch(
44+
"pygitguardian.client.GGClient.read_metadata",
45+
return_value=Detail("Service is unavailable", 503),
46+
)
47+
def test_server_unavailable(self, _: Mock, cli_fs_runner):
48+
"""
49+
GIVEN a server that is unavailable
50+
WHEN scan repo is called
51+
THEN it should return 0
52+
"""
53+
result = cli_fs_runner.invoke(
54+
cli,
55+
["secret", "scan", "repo", "https://github.com/gitguardian/ggshield.git"],
56+
)
57+
assert_invoke_exited_with(result, ExitCode.GITGUARDIAN_SERVER_UNAVAILABLE)
58+
assert "GitGuardian server is not responding" in result.output

tests/unit/core/test_client.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,18 @@
1010

1111
from ggshield.core.client import check_client_api_key, create_client_from_config
1212
from ggshield.core.config import Config
13-
from ggshield.core.errors import APIKeyCheckError, UnexpectedError, UnknownInstanceError
13+
from ggshield.core.errors import (
14+
APIKeyCheckError,
15+
ServiceUnavailableError,
16+
UnexpectedError,
17+
UnknownInstanceError,
18+
)
1419

1520

1621
@pytest.mark.parametrize(
1722
("response", "error_class"),
1823
(
19-
(Detail("Guru Meditation", 500), UnexpectedError),
24+
(Detail("Guru Meditation", 500), ServiceUnavailableError),
2025
(Detail("Nobody here", 404), UnexpectedError),
2126
(Detail("Unauthorized", 401), APIKeyCheckError),
2227
),
@@ -42,6 +47,7 @@ def test_check_client_api_key_network_error():
4247
"""
4348
client_mock = Mock()
4449
client_mock.health_check = Mock(side_effect=requests.exceptions.ConnectionError)
50+
client_mock.read_metadata = Mock(return_value=Detail("Not found", 404))
4551
with pytest.raises(UnexpectedError):
4652
check_client_api_key(client_mock)
4753

0 commit comments

Comments
 (0)