Skip to content

Commit bd15d80

Browse files
committed
add http healthcheck, subcommands
1 parent 4fd735a commit bd15d80

File tree

2 files changed

+107
-14
lines changed

2 files changed

+107
-14
lines changed

src/common/core/cli/healthcheck.py

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,99 @@
11
import argparse
22
import socket
3+
import urllib.parse
4+
5+
import requests
36

47

58
def get_args(argv: list[str]) -> argparse.Namespace:
69
parser = argparse.ArgumentParser(
7-
description="Check if the API is able to accept local TCP connections.",
10+
description="Health checks",
11+
)
12+
subcommands = parser.add_subparsers(dest="subcommand", required=True)
13+
tcp_parser = subcommands.add_parser(
14+
"tcp", help="Check if the API is able to accept local TCP connections"
815
)
9-
parser.add_argument(
16+
tcp_parser.add_argument(
1017
"--port",
1118
"-p",
1219
type=int,
1320
default=8000,
1421
help="Port to check the API on (default: 8000)",
1522
)
16-
parser.add_argument(
23+
tcp_parser.add_argument(
1724
"--timeout",
1825
"-t",
1926
type=int,
2027
default=1,
2128
help="Socket timeout for the connection attempt in seconds (default: 1)",
2229
)
30+
http_parser = subcommands.add_parser(
31+
"http", help="Check if the API is able to serve HTTP requests"
32+
)
33+
http_parser.add_argument(
34+
"--port",
35+
"-p",
36+
type=int,
37+
default=8000,
38+
help="Port to check the API on (default: 8000)",
39+
)
40+
http_parser.add_argument(
41+
"--timeout",
42+
"-t",
43+
type=int,
44+
default=1,
45+
help="Request timeout in seconds (default: 1)",
46+
)
47+
http_parser.add_argument(
48+
"path",
49+
nargs="?",
50+
type=str,
51+
default="/health/liveness",
52+
help="Request path (default: /health/liveness)",
53+
)
2354
return parser.parse_args(argv)
2455

2556

26-
def main(argv: list[str]) -> None:
27-
args = get_args(argv)
57+
def check_tcp_connection(
58+
port: int,
59+
timeout_seconds: int,
60+
) -> None:
2861
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
29-
sock.settimeout(args.timeout)
62+
sock.settimeout(timeout_seconds)
3063
try:
31-
sock.connect(("127.0.0.1", args.port))
64+
sock.connect(("127.0.0.1", port))
3265
except socket.error as e:
33-
print(f"Failed: {e} {args.port=}")
66+
print(f"Failed: {e} {port=}")
3467
exit(1)
3568
else:
3669
exit(0)
3770
finally:
3871
sock.close()
72+
73+
74+
def check_http_response(
75+
port: int,
76+
timeout_seconds: int,
77+
path: str,
78+
) -> None:
79+
url = urllib.parse.urljoin(f"http://127.0.0.1:{port}", path)
80+
requests.get(
81+
url,
82+
timeout=timeout_seconds,
83+
).raise_for_status()
84+
85+
86+
def main(argv: list[str]) -> None:
87+
args = get_args(argv)
88+
match args.subcommand:
89+
case "tcp":
90+
check_tcp_connection(
91+
port=args.port,
92+
timeout_seconds=args.timeout,
93+
)
94+
case "http":
95+
check_http_response(
96+
port=args.port,
97+
timeout_seconds=args.timeout,
98+
path=args.path,
99+
)

tests/integration/core/test_main.py

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,32 +25,64 @@ def test_main__non_overridden_args__defaults_to_django(
2525
assert capsys.readouterr().out[:-1] == expected_out
2626

2727

28-
def test_main__healthcheck__no_server__runs_expected(
28+
def test_main__healthcheck_tcp__no_server__runs_expected(
2929
capsys: pytest.CaptureFixture[str],
3030
) -> None:
3131
# Given
32-
argv = ["flagsmith", "healthcheck"]
32+
argv = ["flagsmith", "healthcheck", "tcp"]
3333

3434
# When
3535
with pytest.raises(SystemExit) as exc_info:
3636
main(argv)
3737

3838
# Then
39-
assert "Connection refused args.port=8000" in capsys.readouterr().out
39+
assert "Connection refused port=8000" in capsys.readouterr().out
4040
assert exc_info.value.code == 1
4141

4242

43-
def test_main__healtcheck__server__runs_expected(
43+
def test_main__healtcheck_tcp__server__runs_expected(
4444
unused_tcp_port: int,
4545
http_server: HTTPServer,
46-
capsys: pytest.CaptureFixture[str],
4746
) -> None:
4847
# Given
49-
argv = ["flagsmith", "healthcheck", "--port", str(unused_tcp_port)]
48+
argv = ["flagsmith", "healthcheck", "tcp", "--port", str(unused_tcp_port)]
5049

5150
# When
5251
with pytest.raises(SystemExit) as exc_info:
5352
main(argv)
5453

5554
# Then
5655
assert exc_info.value.code == 0
56+
57+
58+
def test_main__healtcheck_http__no_server__runs_expected(
59+
capsys: pytest.CaptureFixture[str],
60+
) -> None:
61+
# Given
62+
argv = ["flagsmith", "healthcheck", "http"]
63+
64+
# When & Then
65+
with pytest.raises(Exception):
66+
main(argv)
67+
68+
69+
@pytest.mark.parametrize(
70+
"argv, expected_path",
71+
(
72+
(["flagsmith", "healthcheck", "http"], "/health/liveness"),
73+
(["flagsmith", "healthcheck", "http", "health/readiness"], "/health/readiness"),
74+
),
75+
)
76+
def test_main__healthcheck_http__server__runs_expected(
77+
unused_tcp_port: int,
78+
http_server: HTTPServer,
79+
argv: list[str],
80+
expected_path: str,
81+
) -> None:
82+
# Given
83+
argv.extend(["--port", str(unused_tcp_port)])
84+
85+
http_server.expect_request(expected_path).respond_with_data(status=200)
86+
87+
# When & Then
88+
main(argv)

0 commit comments

Comments
 (0)