correct NOTIFY config reload dying on error
This commit is contained in:
committed by
Steve Chavez
parent
17af56adb1
commit
6557f1f9c0
+23
-16
@@ -1,4 +1,5 @@
|
|||||||
{-# LANGUAGE CPP #-}
|
{-# LANGUAGE CPP #-}
|
||||||
|
{-# LANGUAGE LambdaCase #-}
|
||||||
{-# LANGUAGE MultiWayIf #-}
|
{-# LANGUAGE MultiWayIf #-}
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
|
|
||||||
@@ -66,7 +67,7 @@ main = do
|
|||||||
CLI{cliCommand, cliPath} <- readCLIShowHelp env
|
CLI{cliCommand, cliPath} <- readCLIShowHelp env
|
||||||
|
|
||||||
-- build the 'AppConfig' from the config file path
|
-- build the 'AppConfig' from the config file path
|
||||||
conf <- readValidateConfig mempty env cliPath
|
conf <- either panic identity <$> readConfig mempty env cliPath
|
||||||
|
|
||||||
-- These are config values that can't be reloaded at runtime. Reloading some of them would imply restarting the web server.
|
-- These are config values that can't be reloaded at runtime. Reloading some of them would imply restarting the web server.
|
||||||
let
|
let
|
||||||
@@ -103,10 +104,10 @@ main = do
|
|||||||
-- Config that can change at runtime
|
-- Config that can change at runtime
|
||||||
refConf <- newIORef conf
|
refConf <- newIORef conf
|
||||||
|
|
||||||
let configRereader = reReadConfig pool gucConfigEnabled env cliPath refConf
|
let configRereader startingUp = reReadConfig startingUp pool gucConfigEnabled env cliPath refConf
|
||||||
|
|
||||||
-- re-read and override the config if db-load-guc-config is true
|
-- re-read and override the config if db-load-guc-config is true
|
||||||
when gucConfigEnabled configRereader
|
when gucConfigEnabled $ configRereader True
|
||||||
|
|
||||||
case cliCommand of
|
case cliCommand of
|
||||||
CmdDumpConfig ->
|
CmdDumpConfig ->
|
||||||
@@ -148,13 +149,13 @@ main = do
|
|||||||
|
|
||||||
-- Re-read the config on SIGUSR2
|
-- Re-read the config on SIGUSR2
|
||||||
void $ installHandler sigUSR2 (
|
void $ installHandler sigUSR2 (
|
||||||
Catch $ configRereader >> putStrLn ("Config reloaded" :: Text)
|
Catch $ configRereader False
|
||||||
) Nothing
|
) Nothing
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
-- reload schema cache + config on NOTIFY
|
-- reload schema cache + config on NOTIFY
|
||||||
when dbChannelEnabled $
|
when dbChannelEnabled $
|
||||||
listener dbUri dbChannel pool refConf refDbStructure mvarConnectionStatus connWorker configRereader
|
listener dbUri dbChannel pool refConf refDbStructure mvarConnectionStatus connWorker $ configRereader False
|
||||||
|
|
||||||
-- ask for the OS time at most once per second
|
-- ask for the OS time at most once per second
|
||||||
getTime <- mkAutoUpdate defaultUpdateSettings {updateAction = getCurrentTime}
|
getTime <- mkAutoUpdate defaultUpdateSettings {updateAction = getCurrentTime}
|
||||||
@@ -310,7 +311,7 @@ loadSchemaCache pool actualPgVersion refConf refDbStructure = do
|
|||||||
It uses the connectionWorker in case the LISTEN connection dies.
|
It uses the connectionWorker in case the LISTEN connection dies.
|
||||||
-}
|
-}
|
||||||
listener :: ByteString -> Text -> P.Pool -> IORef AppConfig -> IORef (Maybe DbStructure) -> MVar ConnectionStatus -> IO () -> IO () -> IO ()
|
listener :: ByteString -> Text -> P.Pool -> IORef AppConfig -> IORef (Maybe DbStructure) -> MVar ConnectionStatus -> IO () -> IO () -> IO ()
|
||||||
listener dbUri dbChannel pool refConf refDbStructure mvarConnectionStatus connWorker configRereader = start
|
listener dbUri dbChannel pool refConf refDbStructure mvarConnectionStatus connWorker configLoader = start
|
||||||
where
|
where
|
||||||
start = do
|
start = do
|
||||||
connStatus <- takeMVar mvarConnectionStatus -- takeMVar makes the thread wait if the MVar is empty(until there's a connection).
|
connStatus <- takeMVar mvarConnectionStatus -- takeMVar makes the thread wait if the MVar is empty(until there's a connection).
|
||||||
@@ -321,14 +322,13 @@ listener dbUri dbChannel pool refConf refDbStructure mvarConnectionStatus connWo
|
|||||||
Right db -> do
|
Right db -> do
|
||||||
putStrLn $ "Listening for notifications on the " <> dbChannel <> " channel"
|
putStrLn $ "Listening for notifications on the " <> dbChannel <> " channel"
|
||||||
let channelToListen = N.toPgIdentifier dbChannel
|
let channelToListen = N.toPgIdentifier dbChannel
|
||||||
cfLoader = configRereader >> putStrLn ("Config reloaded" :: Text)
|
|
||||||
scLoader = void $ loadSchemaCache pool actualPgVersion refConf refDbStructure -- It's not necessary to check the loadSchemaCache success here. If the connection drops, the thread will die and proceed to recover below.
|
scLoader = void $ loadSchemaCache pool actualPgVersion refConf refDbStructure -- It's not necessary to check the loadSchemaCache success here. If the connection drops, the thread will die and proceed to recover below.
|
||||||
N.listen db channelToListen
|
N.listen db channelToListen
|
||||||
N.waitForNotifications (\_ msg ->
|
N.waitForNotifications (\_ msg ->
|
||||||
if | BS.null msg -> scLoader -- reload the schema cache
|
if | BS.null msg -> scLoader -- reload the schema cache
|
||||||
| msg == "reload schema" -> scLoader -- reload the schema cache
|
| msg == "reload schema" -> scLoader -- reload the schema cache
|
||||||
| msg == "reload config" -> cfLoader -- reload the config
|
| msg == "reload config" -> configLoader -- reload the config
|
||||||
| otherwise -> pure () -- Do nothing if anything else than an empty message is sent
|
| otherwise -> pure () -- Do nothing if anything else than an empty message is sent
|
||||||
) db
|
) db
|
||||||
_ -> die errorMessage)
|
_ -> die errorMessage)
|
||||||
(\_ -> do -- if the thread dies, we try to recover
|
(\_ -> do -- if the thread dies, we try to recover
|
||||||
@@ -341,12 +341,19 @@ listener dbUri dbChannel pool refConf refDbStructure mvarConnectionStatus connWo
|
|||||||
retryMessage = "Retrying listening for notifications on the " <> dbChannel <> " channel.." :: Text
|
retryMessage = "Retrying listening for notifications on the " <> dbChannel <> " channel.." :: Text
|
||||||
|
|
||||||
-- | Re-reads the config at runtime.
|
-- | Re-reads the config at runtime.
|
||||||
-- | If it panics(config path was changed, invalid setting), it'll show an error but won't kill the main thread.
|
reReadConfig :: Bool -> P.Pool -> Bool -> Environment -> Maybe FilePath -> IORef AppConfig -> IO ()
|
||||||
reReadConfig :: P.Pool -> Bool -> Environment -> Maybe FilePath -> IORef AppConfig -> IO ()
|
reReadConfig startingUp pool gucConfigEnabled env path refConf = do
|
||||||
reReadConfig pool gucConfigEnabled env path refConf = do
|
|
||||||
dbSettings <- if gucConfigEnabled then loadDbSettings pool else pure []
|
dbSettings <- if gucConfigEnabled then loadDbSettings pool else pure []
|
||||||
conf <- readValidateConfig dbSettings env path
|
readConfig dbSettings env path >>= \case
|
||||||
atomicWriteIORef refConf conf
|
Left err ->
|
||||||
|
if startingUp
|
||||||
|
then panic err -- die on invalid config if the program is starting up
|
||||||
|
else hPutStrLn stderr $ "Failed config reload. " <> err
|
||||||
|
Right conf -> do
|
||||||
|
atomicWriteIORef refConf conf
|
||||||
|
if startingUp
|
||||||
|
then pass
|
||||||
|
else putStrLn ("Config reloaded" :: Text)
|
||||||
|
|
||||||
-- | Dump DbStructure schema to JSON
|
-- | Dump DbStructure schema to JSON
|
||||||
dumpSchema :: P.Pool -> AppConfig -> IO LBS.ByteString
|
dumpSchema :: P.Pool -> AppConfig -> IO LBS.ByteString
|
||||||
|
|||||||
+16
-22
@@ -30,7 +30,7 @@ module PostgREST.Config ( prettyVersion
|
|||||||
, Environment
|
, Environment
|
||||||
, readCLIShowHelp
|
, readCLIShowHelp
|
||||||
, readEnvironment
|
, readEnvironment
|
||||||
, readValidateConfig
|
, readConfig
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
@@ -44,6 +44,7 @@ import Control.Lens (preview)
|
|||||||
import Control.Monad (fail)
|
import Control.Monad (fail)
|
||||||
import Crypto.JWT (JWKSet, StringOrURI, stringOrUri)
|
import Crypto.JWT (JWKSet, StringOrURI, stringOrUri)
|
||||||
import Data.Aeson (encode, toJSON)
|
import Data.Aeson (encode, toJSON)
|
||||||
|
import Data.Either.Combinators (mapLeft)
|
||||||
import Data.List (lookup)
|
import Data.List (lookup)
|
||||||
import Data.List.NonEmpty (fromList, toList)
|
import Data.List.NonEmpty (fromList, toList)
|
||||||
import Data.Maybe (fromJust)
|
import Data.Maybe (fromJust)
|
||||||
@@ -52,7 +53,6 @@ import Data.Text (dropEnd, dropWhileEnd, filter,
|
|||||||
intercalate, pack, replace, splitOn,
|
intercalate, pack, replace, splitOn,
|
||||||
strip, stripPrefix, take, toLower,
|
strip, stripPrefix, take, toLower,
|
||||||
toTitle, unpack)
|
toTitle, unpack)
|
||||||
import Data.Text.IO (hPutStrLn)
|
|
||||||
import Data.Version (versionBranch)
|
import Data.Version (versionBranch)
|
||||||
import Development.GitRev (gitHash)
|
import Development.GitRev (gitHash)
|
||||||
import Numeric (readOct, showOct)
|
import Numeric (readOct, showOct)
|
||||||
@@ -330,22 +330,18 @@ instance JustIfMaybe a (Maybe a) where
|
|||||||
justIfMaybe a = Just a
|
justIfMaybe a = Just a
|
||||||
|
|
||||||
-- | Parse the config file
|
-- | Parse the config file
|
||||||
readAppConfig :: [(Text, Text)] -> Environment -> Maybe FilePath -> IO AppConfig
|
readAppConfig :: [(Text, Text)] -> Environment -> Maybe FilePath -> IO (Either Text AppConfig)
|
||||||
readAppConfig dbSettings env optPath = do
|
readAppConfig dbSettings env optPath = do
|
||||||
-- Now read the actual config file
|
-- Now read the actual config file
|
||||||
conf <- case optPath of
|
conf <- case optPath of
|
||||||
Just cfgPath -> catches (C.load cfgPath)
|
Just cfgPath -> C.load cfgPath `catches`
|
||||||
[ Handler (\(ex :: IOError) -> exitErr $ "Cannot open config file:\n\t" <> show ex)
|
[ Handler (\(ex :: IOError) -> panic $ "Cannot open config file: " <> show ex)
|
||||||
, Handler (\(C.ParseError err) -> exitErr $ "Error parsing config file:\n" <> err)
|
, Handler (\(C.ParseError err) -> panic $ "Error parsing config file: " <> err)
|
||||||
]
|
]
|
||||||
-- if no filename provided, start with an empty map to read config from environment
|
-- if no filename provided, start with an empty map to read config from environment
|
||||||
Nothing -> return M.empty
|
Nothing -> return M.empty
|
||||||
|
|
||||||
case C.runParser parseConfig conf of
|
pure $ mapLeft ("Error in config: " <>) $ C.runParser parseConfig conf
|
||||||
Left err ->
|
|
||||||
exitErr $ "Error in config:\n\t" <> err
|
|
||||||
Right appConf ->
|
|
||||||
return appConf
|
|
||||||
|
|
||||||
where
|
where
|
||||||
parseConfig =
|
parseConfig =
|
||||||
@@ -395,7 +391,7 @@ readAppConfig dbSettings env optPath = do
|
|||||||
parseSocketFileMode :: C.Key -> C.Parser C.Config FileMode
|
parseSocketFileMode :: C.Key -> C.Parser C.Config FileMode
|
||||||
parseSocketFileMode k =
|
parseSocketFileMode k =
|
||||||
optString k >>= \case
|
optString k >>= \case
|
||||||
Nothing -> pure $ 432 -- return default 660 mode if no value was provided
|
Nothing -> pure 432 -- return default 660 mode if no value was provided
|
||||||
Just fileModeText ->
|
Just fileModeText ->
|
||||||
case (readOct . unpack) fileModeText of
|
case (readOct . unpack) fileModeText of
|
||||||
[] ->
|
[] ->
|
||||||
@@ -518,16 +514,14 @@ readAppConfig dbSettings env optPath = do
|
|||||||
splitOnCommas (C.String s) = strip <$> splitOn "," s
|
splitOnCommas (C.String s) = strip <$> splitOn "," s
|
||||||
splitOnCommas _ = []
|
splitOnCommas _ = []
|
||||||
|
|
||||||
exitErr :: Text -> IO a
|
-- | Reads the config and overrides its parameters from files, env vars or db settings.
|
||||||
exitErr err = do
|
readConfig :: [(Text, Text)] -> Environment -> Maybe FilePath -> IO (Either Text AppConfig)
|
||||||
hPutStrLn stderr err
|
readConfig dbSettings env path =
|
||||||
exitFailure
|
readAppConfig dbSettings env path >>= \case
|
||||||
|
Left err -> pure $ Left err
|
||||||
-- | Parse the AppConfig and validate it. Overrides the config options from env vars or db settings. Panics on invalid config options.
|
Right appConf -> do
|
||||||
readValidateConfig :: [(Text, Text)] -> Environment -> Maybe FilePath -> IO AppConfig
|
conf <- loadDbUriFile =<< loadSecretFile appConf
|
||||||
readValidateConfig dbSettings env path = do
|
pure $ Right $ conf { configJWKS = parseSecret <$> configJwtSecret conf}
|
||||||
conf <- loadDbUriFile =<< loadSecretFile =<< readAppConfig dbSettings env path
|
|
||||||
return $ conf { configJWKS = parseSecret <$> configJwtSecret conf}
|
|
||||||
|
|
||||||
type Environment = M.Map [Char] Text
|
type Environment = M.Map [Char] Text
|
||||||
|
|
||||||
|
|||||||
Vendored
+12
@@ -1955,3 +1955,15 @@ begin
|
|||||||
perform pg_notify('pgrst', 'reload config');
|
perform pg_notify('pgrst', 'reload config');
|
||||||
perform pg_notify('pgrst', 'reload schema');
|
perform pg_notify('pgrst', 'reload schema');
|
||||||
end $_$ volatile security definer language plpgsql ;
|
end $_$ volatile security definer language plpgsql ;
|
||||||
|
|
||||||
|
create or replace function test.invalid_role_claim_key_reload() returns void as $_$
|
||||||
|
begin
|
||||||
|
alter role postgrest_test_authenticator set pgrst."jwt-role-claim-key" = 'test';
|
||||||
|
perform pg_notify('pgrst', 'reload config');
|
||||||
|
end $_$ volatile security definer language plpgsql ;
|
||||||
|
|
||||||
|
create or replace function test.reset_invalid_role_claim_key() returns void as $_$
|
||||||
|
begin
|
||||||
|
alter role postgrest_test_authenticator set pgrst."jwt-role-claim-key" = '."a"."role"';
|
||||||
|
perform pg_notify('pgrst', 'reload config');
|
||||||
|
end $_$ volatile security definer language plpgsql ;
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ def run(configpath=None, stdin=None, env=None, port=None):
|
|||||||
if configpath:
|
if configpath:
|
||||||
command.append(configpath)
|
command.append(configpath)
|
||||||
|
|
||||||
process = subprocess.Popen(command, stdin=subprocess.PIPE, env=env)
|
process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
process.stdin.write(stdin or b"")
|
process.stdin.write(stdin or b"")
|
||||||
@@ -590,3 +590,19 @@ def test_max_rows_notify_reload(defaultenv):
|
|||||||
|
|
||||||
# reset max-rows config on the db
|
# reset max-rows config on the db
|
||||||
postgrest.session.post("/rpc/reset_max_rows_config")
|
postgrest.session.post("/rpc/reset_max_rows_config")
|
||||||
|
|
||||||
|
def invalid_role_claim_key_notify_reload(defaultenv):
|
||||||
|
"NOTIFY reload config should show an error if role-claim-key is invalid"
|
||||||
|
|
||||||
|
env = {
|
||||||
|
**defaultenv,
|
||||||
|
"PGRST_DB_LOAD_GUC_CONFIG": "true",
|
||||||
|
"PGRST_DB_CHANNEL_ENABLED": "true",
|
||||||
|
}
|
||||||
|
|
||||||
|
with run(env=env) as postgrest:
|
||||||
|
postgrest.session.post("/rpc/invalid_role_claim_key_reload")
|
||||||
|
|
||||||
|
assert "failed to parse role-claim-key value" in str(postgrest.process.stderr.readline())
|
||||||
|
|
||||||
|
postgrest.session.post("/rpc/reset_invalid_role_claim_key")
|
||||||
|
|||||||
Reference in New Issue
Block a user