Skip to content

Commit a9a1763

Browse files
taimoorzaeemsteve-chavez
authored andcommitted
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 <[email protected]>
1 parent ab2cd76 commit a9a1763

File tree

10 files changed

+273
-6
lines changed

10 files changed

+273
-6
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
1414
### Added
1515

1616
- Bounded JWT cache using the SIEVE algorithm by @mkleczek in #4084
17+
- Add `--ready` flag for postgrest healthcheck by @taimoorzaeem in #4239
1718

1819
### Changed
1920

docs/explanations/install.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ To avoid having to install the database at all, you can run both it and the serv
142142
ports:
143143
- "3000:3000"
144144
environment:
145+
PGRST_SERVER_HOST: localhost # necessary for `postgrest --ready` flag to work
145146
PGRST_DB_URI: postgres://app_user:password@db:5432/app_db
146147
PGRST_OPENAPI_SERVER_PROXY_URI: http://127.0.0.1:3000
147148
depends_on:

docs/references/cli.rst

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ PostgREST provides a CLI with the options listed below:
77

88
.. code:: text
99
10-
Usage: postgrest [-v|--version] [-e|--example] [--dump-config | --dump-schema]
10+
Usage: postgrest [-v|--version] [-e|--example] [--dump-config | --dump-schema | --ready]
1111
[FILENAME]
1212
1313
PostgREST / create a REST API to an existing Postgres
@@ -20,6 +20,8 @@ PostgREST provides a CLI with the options listed below:
2020
--dump-config Dump loaded configuration and exit
2121
--dump-schema Dump loaded schema as JSON and exit (for debugging,
2222
output structure is unstable)
23+
--ready Checks the health of PostgREST by doing a request on
24+
the admin server /ready endpoint
2325
FILENAME Path to configuration file
2426
2527
FILENAME
@@ -71,3 +73,17 @@ Dump Schema
7173
$ postgrest --dump-schema
7274
7375
Dumps the schema cache in JSON format.
76+
77+
Ready Flag
78+
----------
79+
80+
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.
81+
82+
.. code-block:: bash
83+
84+
$ postgrest --ready
85+
OK: http://localhost:3001/ready
86+
87+
.. note::
88+
89+
The ``--ready`` flag cannot be used when :ref:`server-host` is configured with special hostnames. We suggest to change it to ``localhost``.

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
@@ -118,6 +119,7 @@ library
118119
, hasql-pool >= 1.0.1 && < 1.1
119120
, hasql-transaction >= 1.0.1 && < 1.2
120121
, heredoc >= 0.2 && < 0.3
122+
, http-client >= 0.7.19 && < 0.8
121123
, http-types >= 0.12.2 && < 0.13
122124
, insert-ordered-containers >= 0.2.2 && < 0.3
123125
, iproute >= 1.7.0 && < 1.8

src/PostgREST/CLI.hs

Lines changed: 30 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 #-}
@@ -24,23 +25,34 @@ import PostgREST.Version (prettyVersion)
2425

2526
import qualified PostgREST.App as App
2627
import qualified PostgREST.AppState as AppState
28+
import qualified PostgREST.Client as Client
2729
import qualified PostgREST.Config as Config
2830

2931
import Protolude
3032

3133

3234
main :: CLI -> IO ()
3335
main CLI{cliCommand, cliPath} = do
34-
conf@AppConfig{..} <-
36+
conf <-
3537
either panic identity <$> Config.readAppConfig mempty cliPath Nothing mempty mempty
38+
case cliCommand of
39+
Client adminCmd -> runClientCommand conf adminCmd
40+
Run runCmd -> runAppCommand conf runCmd
3641

42+
-- | Run command using http-client to communicate with an already running postgrest
43+
runClientCommand :: AppConfig -> ClientCommand -> IO ()
44+
runClientCommand conf CmdReady = Client.ready conf
45+
46+
-- | Run postgrest with command
47+
runAppCommand :: AppConfig -> RunCommand -> IO ()
48+
runAppCommand conf@AppConfig{..} runCmd = do
3749
-- Per https://github.com/PostgREST/postgrest/issues/268, we want to
3850
-- explicitly close the connections to PostgreSQL on shutdown.
3951
-- 'AppState.destroy' takes care of that.
4052
bracket
4153
(AppState.init conf)
4254
AppState.destroy
43-
(\appState -> case cliCommand of
55+
(\appState -> case runCmd of
4456
CmdDumpConfig -> do
4557
when configDbConfig $ AppState.readInDbConfig True appState
4658
putStr . Config.toText =<< AppState.getConfig appState
@@ -71,6 +83,13 @@ data CLI = CLI
7183
}
7284

7385
data Command
86+
= Client ClientCommand
87+
| Run RunCommand
88+
89+
data ClientCommand
90+
= CmdReady
91+
92+
data RunCommand
7493
= CmdRun
7594
| CmdDumpConfig
7695
| CmdDumpSchema
@@ -105,7 +124,7 @@ readCLIShowHelp =
105124
cliParser :: O.Parser CLI
106125
cliParser =
107126
CLI
108-
<$> (dumpConfigFlag <|> dumpSchemaFlag)
127+
<$> (dumpConfigFlag <|> dumpSchemaFlag <|> readyFlag)
109128
<*> O.optional configFileOption
110129

111130
configFileOption =
@@ -114,15 +133,21 @@ readCLIShowHelp =
114133
<> O.help "Path to configuration file"
115134

116135
dumpConfigFlag =
117-
O.flag CmdRun CmdDumpConfig $
136+
O.flag (Run CmdRun) (Run CmdDumpConfig) $
118137
O.long "dump-config"
119138
<> O.help "Dump loaded configuration and exit"
120139

121140
dumpSchemaFlag =
122-
O.flag CmdRun CmdDumpSchema $
141+
O.flag (Run CmdRun) (Run CmdDumpSchema) $
123142
O.long "dump-schema"
124143
<> O.help "Dump loaded schema as JSON and exit (for debugging, output structure is unstable)"
125144

145+
readyFlag =
146+
O.flag (Run CmdRun) (Client CmdReady) $
147+
O.long "ready"
148+
<> O.help "Checks the health of PostgREST by doing a request on the admin server /ready endpoint"
149+
150+
126151
exampleConfigFile :: [Char]
127152
exampleConfigFile =
128153
[str|## Admin server used for checks. It's disabled by default unless a port is specified.

src/PostgREST/Client.hs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
{-|
2+
Module : PostgREST.Client
3+
Description : PostgREST HTTP client
4+
-}
5+
{-# LANGUAGE NamedFieldPuns #-}
6+
module PostgREST.Client
7+
( ready
8+
) where
9+
10+
import qualified Data.Text as T
11+
import qualified Network.HTTP.Client as HC
12+
import qualified Network.HTTP.Types.Status as HTTP
13+
14+
import Network.HTTP.Client (HttpException (..))
15+
import System.IO (hFlush)
16+
17+
import PostgREST.Config (AppConfig (..))
18+
import PostgREST.Network (isSpecialHostName)
19+
20+
import Protolude
21+
22+
data PgrstClientError
23+
= NoAdminServer
24+
| NoSpecialHostNamesAllowed Text
25+
| PostgRESTNotReady Text
26+
| HTTPConnectionRefused Text
27+
| HTTPExceptionInvalidURL Text
28+
29+
-- | This is invoked by the CLI "--ready" flag.
30+
-- The http-client sends and a request to /ready endpoint
31+
-- and exits with success or failure.
32+
ready :: AppConfig -> IO ()
33+
ready AppConfig{configAdminServerHost, configAdminServerPort} = do
34+
35+
client <- HC.newManager HC.defaultManagerSettings
36+
readyURL <- getURL
37+
req <- HC.parseRequest (T.unpack readyURL) `catch` handleHttpException
38+
resp <- HC.httpLbs req client `catch` handleHttpException
39+
40+
let status = HC.responseStatus resp
41+
42+
if status >= HTTP.status200 && status < HTTP.status300
43+
then printAndExitWithSuccess $ "OK: " <> readyURL
44+
else printAndExitWithFailure $ clientErrorMsg (PostgRESTNotReady readyURL)
45+
where
46+
getURL :: IO Text
47+
getURL =
48+
-- Here, we have three cases:
49+
-- 1. If the admin port config is not defined, we exit
50+
-- with "no admin server error"
51+
-- 2. Otherwise, if admin server is running, then we check if
52+
-- postgrest server-host is configured with special hostname like "*4",
53+
-- if it is, we fail with "no special hostname allowed with "--ready".
54+
-- The reason for this is that we can't know the actual address.
55+
-- 3. Finally, if we know the "actual" hostname and the port, then we
56+
-- construct the URL and return it.
57+
case configAdminServerPort of
58+
Nothing -> printAndExitWithFailure $ clientErrorMsg NoAdminServer
59+
Just port ->
60+
if isSpecialHostName configAdminServerHost
61+
then printAndExitWithFailure $ clientErrorMsg (NoSpecialHostNamesAllowed configAdminServerHost)
62+
else return $ makeReadyUrl port
63+
64+
-- NOTE: http-client automatically resolves hostnames
65+
makeReadyUrl :: Int -> Text
66+
makeReadyUrl p = "http://" <> wrapIfIpv6 configAdminServerHost <> ":" <> (T.pack . show) p <> "/ready"
67+
where
68+
-- IPv6 needs to wrapped in [], it has ':' as separator
69+
wrapIfIpv6 :: Text -> Text
70+
wrapIfIpv6 s
71+
| T.any (== ':') s = "[" <> s <> "]"
72+
| otherwise = s
73+
74+
-- | Handle HTTP exception for "http-client" requests
75+
handleHttpException :: HttpException -> IO a
76+
handleHttpException (HttpExceptionRequest req _) = do
77+
let url = show (HC.getUri req)
78+
printAndExitWithFailure $ clientErrorMsg (HTTPConnectionRefused $ T.pack url)
79+
handleHttpException (InvalidUrlException url _) = do
80+
printAndExitWithFailure $ clientErrorMsg (HTTPExceptionInvalidURL $ T.pack url)
81+
82+
-- | Print the message on stdout and exit with success
83+
printAndExitWithSuccess :: Text -> IO a
84+
printAndExitWithSuccess msg = putStrLn (T.unpack msg) >> hFlush stdout >> exitSuccess
85+
86+
-- | Print the message on stderr and exit with failure
87+
printAndExitWithFailure :: Text -> IO a
88+
printAndExitWithFailure msg = hPutStrLn stderr (T.unpack msg) >> hFlush stderr >> exitWith (ExitFailure 1)
89+
90+
-- | Pgrst client error to error message
91+
clientErrorMsg :: PgrstClientError -> Text
92+
clientErrorMsg err = "ERROR: " <>
93+
case err of
94+
NoAdminServer -> "Admin server is not running. Please check admin-server-port config."
95+
NoSpecialHostNamesAllowed host ->
96+
"The `--ready` flag cannot be used when server-host is configured as \"" <> host <> "\". "
97+
<> "Please update your server-host config to \"localhost\"."
98+
PostgRESTNotReady url -> url
99+
HTTPConnectionRefused url -> "connection refused to " <> url
100+
HTTPExceptionInvalidURL url -> "invalid url - " <> url

src/PostgREST/Network.hs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module PostgREST.Network
22
( resolveSocketToAddress
33
, escapeHostName
4+
, isSpecialHostName
45
) where
56

67
import Data.String (IsString (..))
@@ -49,3 +50,12 @@ escapeHostName "!4" = "0.0.0.0"
4950
escapeHostName "*6" = "0.0.0.0"
5051
escapeHostName "!6" = "0.0.0.0"
5152
escapeHostName h = h
53+
54+
-- | Check if a hostname is special
55+
isSpecialHostName :: Text -> Bool
56+
isSpecialHostName "*" = True
57+
isSpecialHostName "*4" = True
58+
isSpecialHostName "!4" = True
59+
isSpecialHostName "*6" = True
60+
isSpecialHostName "!6" = True
61+
isSpecialHostName _ = False

test/io/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,9 @@ def hpctixfile():
9595
# astronomically low.
9696
test = uuid.uuid4().hex[:12]
9797
return tixfile.with_suffix(f".{test}.tix")
98+
99+
100+
def get_admin_host_and_port_from_config(config):
101+
admin_host = config.get("PGRST_ADMIN_SERVER_HOST", config["PGRST_SERVER_HOST"])
102+
admin_port = config["PGRST_ADMIN_SERVER_PORT"]
103+
return (admin_host, admin_port)

test/io/postgrest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ class PostgrestProcess:
5757
admin: object
5858
process: object
5959
session: object
60+
config: object
6061

6162
def read_stdout(self, nlines=1):
6263
"Wait for line(s) on standard output."
@@ -143,6 +144,7 @@ def run(
143144
process=process,
144145
session=PostgrestSession(baseurl),
145146
admin=PostgrestSession(adminurl),
147+
config=env,
146148
)
147149
finally:
148150
remaining_output = process.stdout.read()

0 commit comments

Comments
 (0)