Skip to content

Commit d958101

Browse files
committed
add: --ready flag for postgrest healthcheck
The `--ready` flag is a wrapper around the admin server `/ready` request. This is done through using an http client library in postgrest. Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
1 parent 96d2b69 commit d958101

File tree

7 files changed

+187
-5
lines changed

7 files changed

+187
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
1212
+ The exposed schemas are now listed in the `hint` instead of the `message` field.
1313
- Improve error details of `PGRST301` error by @taimoorzaeem in #4051
1414
- Bounded JWT cache using the SIEVE algorithm by @mkleczek in #4084
15+
- Add `--ready` flag for postgrest healthcheck by @taimoorzaeem in #4239
1516

1617
### Changed
1718

docs/references/cli.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,12 @@ Dump Schema
4949
$ postgrest [--dump-schema]
5050
5151
Dumps the schema cache in JSON format.
52+
53+
Ready Flag
54+
----------
55+
56+
.. code:: bash
57+
58+
$ postgrest [--ready]
59+
60+
Makes a request to the ``/ready`` endpoint of the :ref:`admin_server`. It exits with a return code of ``0`` on success and ``1`` on failure.

postgrest.cabal

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ library
5252
PostgREST.Auth.Types
5353
PostgREST.Cache.Sieve
5454
PostgREST.CLI
55+
PostgREST.Client
5556
PostgREST.Config
5657
PostgREST.Config.Database
5758
PostgREST.Config.JSPath
@@ -117,6 +118,7 @@ library
117118
, hasql-pool >= 1.0.1 && < 1.1
118119
, hasql-transaction >= 1.0.1 && < 1.2
119120
, heredoc >= 0.2 && < 0.3
121+
, http-client >= 0.7.19 && < 0.8
120122
, http-types >= 0.12.2 && < 0.13
121123
, insert-ordered-containers >= 0.2.2 && < 0.3
122124
, iproute >= 1.7.0 && < 1.8

src/PostgREST/CLI.hs

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
{-# LANGUAGE LambdaCase #-}
12
{-# LANGUAGE NamedFieldPuns #-}
23
{-# LANGUAGE QuasiQuotes #-}
34
{-# LANGUAGE RecordWildCards #-}
@@ -12,8 +13,10 @@ import qualified Data.Aeson as JSON
1213
import qualified Data.ByteString.Char8 as BS
1314
import qualified Data.ByteString.Lazy as LBS
1415
import qualified Hasql.Transaction.Sessions as SQL
16+
import qualified Network.HTTP.Types.Status as HTTP
1517
import qualified Options.Applicative as O
1618

19+
import Data.Maybe (fromJust)
1720
import Text.Heredoc (str)
1821

1922
import PostgREST.AppState (AppState)
@@ -24,23 +27,41 @@ import PostgREST.Version (prettyVersion)
2427

2528
import qualified PostgREST.App as App
2629
import qualified PostgREST.AppState as AppState
30+
import qualified PostgREST.Client as Client
2731
import qualified PostgREST.Config as Config
2832

2933
import Protolude
3034

3135

3236
main :: CLI -> IO ()
3337
main CLI{cliCommand, cliPath} = do
34-
conf@AppConfig{..} <-
38+
conf <-
3539
either panic identity <$> Config.readAppConfig mempty cliPath Nothing mempty mempty
40+
case cliCommand of
41+
Admin adminCmd -> runAdminCommand conf adminCmd
42+
Run runCmd -> runAppCommand conf runCmd
3643

44+
-- | Run command using http-client to communicate with an already running postgrest
45+
runAdminCommand :: AppConfig -> AdminCommand -> IO ()
46+
runAdminCommand conf CmdReady = do
47+
status <- Client.ready conf
48+
if status >= HTTP.status200 && status < HTTP.status300
49+
then do
50+
putStrLn $ BS.pack $ "OK: http://:" <> show (fromJust $ configAdminServerPort conf) <> "/ready"
51+
exitSuccess
52+
else
53+
exitWith $ ExitFailure 1
54+
55+
-- | Run postgrest with command
56+
runAppCommand :: AppConfig -> RunCommand -> IO ()
57+
runAppCommand conf@AppConfig{..} runCmd = do
3758
-- Per https://github.com/PostgREST/postgrest/issues/268, we want to
3859
-- explicitly close the connections to PostgreSQL on shutdown.
3960
-- 'AppState.destroy' takes care of that.
4061
bracket
4162
(AppState.init conf)
4263
AppState.destroy
43-
(\appState -> case cliCommand of
64+
(\appState -> case runCmd of
4465
CmdDumpConfig -> do
4566
when configDbConfig $ AppState.readInDbConfig True appState
4667
putStr . Config.toText =<< AppState.getConfig appState
@@ -71,6 +92,13 @@ data CLI = CLI
7192
}
7293

7394
data Command
95+
= Admin AdminCommand
96+
| Run RunCommand
97+
98+
data AdminCommand
99+
= CmdReady
100+
101+
data RunCommand
74102
= CmdRun
75103
| CmdDumpConfig
76104
| CmdDumpSchema
@@ -105,7 +133,7 @@ readCLIShowHelp =
105133
cliParser :: O.Parser CLI
106134
cliParser =
107135
CLI
108-
<$> (dumpConfigFlag <|> dumpSchemaFlag)
136+
<$> (dumpConfigFlag <|> dumpSchemaFlag <|> readyFlag)
109137
<*> O.optional configFileOption
110138

111139
configFileOption =
@@ -114,15 +142,21 @@ readCLIShowHelp =
114142
<> O.help "Path to configuration file"
115143

116144
dumpConfigFlag =
117-
O.flag CmdRun CmdDumpConfig $
145+
O.flag (Run CmdRun) (Run CmdDumpConfig) $
118146
O.long "dump-config"
119147
<> O.help "Dump loaded configuration and exit"
120148

121149
dumpSchemaFlag =
122-
O.flag CmdRun CmdDumpSchema $
150+
O.flag (Run CmdRun) (Run CmdDumpSchema) $
123151
O.long "dump-schema"
124152
<> O.help "Dump loaded schema as JSON and exit (for debugging, output structure is unstable)"
125153

154+
readyFlag =
155+
O.flag (Run CmdRun) (Admin CmdReady) $
156+
O.long "ready"
157+
<> O.help "Checks the health of PostgREST by doing a request on the admin server /ready endpoint"
158+
159+
126160
exampleConfigFile :: [Char]
127161
exampleConfigFile =
128162
[str|## Admin server used for checks. It's disabled by default unless a port is specified.

src/PostgREST/Client.hs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{-|
2+
Module : PostgREST.Client
3+
Description : PostgREST HTTP client
4+
-}
5+
{-# LANGUAGE LambdaCase #-}
6+
{-# LANGUAGE NamedFieldPuns #-}
7+
{-# LANGUAGE ScopedTypeVariables #-}
8+
module PostgREST.Client
9+
( ready
10+
) where
11+
12+
import qualified Data.Text as T
13+
import qualified Network.HTTP.Client as HC
14+
15+
import Network.HTTP.Client (HttpException (..))
16+
import Network.HTTP.Types.Status (Status (..))
17+
import PostgREST.Config (AppConfig (..))
18+
19+
import Protolude
20+
21+
ready :: AppConfig -> IO Status
22+
ready AppConfig{configAdminServerHost, configAdminServerPort} = do
23+
24+
client <- HC.newManager HC.defaultManagerSettings
25+
req <- HC.parseRequest $ -- Create HTTP Request
26+
case configAdminServerPort of
27+
Just port -> "http://" <> host <> ":" <> show port <> "/ready"
28+
Nothing -> panic "Admin Server is not running. Please check your configuration."
29+
30+
resp <- HC.httpLbs req client `catch` \(_ :: HttpException) -> do
31+
let url = show (HC.getUri req)
32+
hPutStrLn stderr $ T.pack $ "postgrest: health check request failed - connection refused to " <> url
33+
exitWith $ ExitFailure 1
34+
35+
return $ HC.responseStatus resp
36+
where
37+
host = escapeHostName $ T.unpack configAdminServerHost
38+
-- We don't need to resolve the host name except the postgrest special
39+
-- addresses. The http-client package automatically resolves the other
40+
-- host names for us.
41+
escapeHostName = \case
42+
"*" -> "0.0.0.0"
43+
"*4" -> "0.0.0.0"
44+
"!4" -> "0.0.0.0"
45+
"*6" -> "[::1]" -- TODO: Not covered by tests
46+
"!6" -> "[::1]" -- TODO: Not covered by tests
47+
h -> h

test/io/postgrest.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,3 +230,19 @@ def is_ipv6(addr):
230230
return True
231231
except OSError:
232232
return False
233+
234+
235+
def run_postgrest_binary(env):
236+
"Runs a the postgrest using the binary and return the running process"
237+
238+
command = [POSTGREST_BIN]
239+
env["HPCTIXFILE"] = hpctixfile()
240+
pgrst_process = subprocess.Popen(command, env=env)
241+
return pgrst_process
242+
243+
244+
def kill_pgrst_process(process):
245+
"Kill the given postgrest process"
246+
247+
process.kill() # Send SIGKILL
248+
process.wait() # Wait for termination

test/io/test_cli.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import yaml
2525

2626
from config import *
27+
from postgrest import *
2728

2829

2930
class ExtraNewLinesDumper(yaml.SafeDumper):
@@ -289,3 +290,75 @@ def test_jwt_aud_config_set_to_invalid_uri(defaultenv):
289290
with pytest.raises(PostgrestError):
290291
dump = cli(["--dump-config"], env=env).split("\n")
291292
assert "jwt-aud should be a string or a valid URI" in dump
293+
294+
295+
def test_cli_ready_flag_success(defaultenv):
296+
"PostgREST ready flag succeeds when ready"
297+
298+
env = {
299+
**defaultenv,
300+
"PGRST_ADMIN_SERVER_PORT": "3001",
301+
}
302+
303+
# Run postgrest process
304+
pgrst_process = run_postgrest_binary(env)
305+
306+
time.sleep(3) # Wait so postgrest gets ready
307+
try:
308+
# Run healthcheck process
309+
command = [POSTGREST_BIN] + ["--ready"]
310+
healthcheck_process = subprocess.run(command, env=env, capture_output=True)
311+
assert healthcheck_process.returncode == 0
312+
assert f"OK: http://:3001/ready" in healthcheck_process.stdout.decode()
313+
finally:
314+
kill_pgrst_process(pgrst_process)
315+
316+
317+
def test_cli_ready_flag_fail(defaultenv):
318+
"PostgREST ready flag fails when not ready"
319+
320+
env = {
321+
**defaultenv,
322+
"PGRST_ADMIN_SERVER_PORT": "3001",
323+
}
324+
325+
# Run postgrest process
326+
pgrst_process = run_postgrest_binary(env)
327+
328+
time.sleep(0) # No waiting and instantly run the healthcheck process.
329+
try:
330+
# Run healthcheck process
331+
command = [POSTGREST_BIN] + ["--ready"]
332+
healthcheck_process = subprocess.run(command, env=env, capture_output=True)
333+
assert healthcheck_process.returncode == 1
334+
assert (
335+
"postgrest: health check request failed - connection refused to"
336+
in healthcheck_process.stderr.decode()
337+
)
338+
finally:
339+
kill_pgrst_process(pgrst_process)
340+
341+
342+
def test_cli_ready_flag_fail_no_admin_server(defaultenv):
343+
"PostgREST --ready flag fail without admin server running"
344+
345+
env = {
346+
**defaultenv,
347+
"PGRST_ADMIN_SERVER_PORT": "",
348+
}
349+
350+
# Run postgrest process
351+
pgrst_process = run_postgrest_binary(env)
352+
353+
time.sleep(3) # Wait so postgrest gets ready
354+
try:
355+
# Run healthcheck process
356+
command = [POSTGREST_BIN] + ["--ready"]
357+
healthcheck_process = subprocess.run(command, env=env, capture_output=True)
358+
assert healthcheck_process.returncode == 1
359+
assert (
360+
"Admin Server is not running. Please check your configuration."
361+
in healthcheck_process.stderr.decode()
362+
)
363+
finally:
364+
kill_pgrst_process(pgrst_process)

0 commit comments

Comments
 (0)