feat: allow to verify the PostgREST version through SQL
This commit is contained in:
@@ -20,6 +20,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
+ Completely optional, define the functions in the database and they will be used automatically everywhere
|
+ Completely optional, define the functions in the database and they will be used automatically everywhere
|
||||||
+ Data representations preserve the ability to write to the original column and require no extra storage or complex triggers (compared to using `GENERATED ALWAYS` columns)
|
+ Data representations preserve the ability to write to the original column and require no extra storage or complex triggers (compared to using `GENERATED ALWAYS` columns)
|
||||||
+ Note: data representations require Postgres 10 (Postgres 11 if using `IN` predicates); data representations are not implemented for RPC
|
+ Note: data representations require Postgres 10 (Postgres 11 if using `IN` predicates); data representations are not implemented for RPC
|
||||||
|
- #2647, Allow to verify the PostgREST version in SQL: `select distinct application_name from pg_stat_activity`. - @laurenceisla
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import qualified Hasql.Pool as SQL
|
|||||||
import qualified Hasql.Session as SQL
|
import qualified Hasql.Session as SQL
|
||||||
import qualified Hasql.Transaction.Sessions as SQL
|
import qualified Hasql.Transaction.Sessions as SQL
|
||||||
import qualified PostgREST.Error as Error
|
import qualified PostgREST.Error as Error
|
||||||
|
import PostgREST.Version (prettyVersion)
|
||||||
|
|
||||||
import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate,
|
import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate,
|
||||||
updateAction)
|
updateAction)
|
||||||
@@ -47,6 +48,7 @@ import Data.Time (ZonedTime, defaultTimeLocale, formatTime,
|
|||||||
import Data.Time.Clock (UTCTime, getCurrentTime)
|
import Data.Time.Clock (UTCTime, getCurrentTime)
|
||||||
|
|
||||||
import PostgREST.Config (AppConfig (..),
|
import PostgREST.Config (AppConfig (..),
|
||||||
|
addFallbackAppName,
|
||||||
readAppConfig)
|
readAppConfig)
|
||||||
import PostgREST.Config.Database (queryDbSettings,
|
import PostgREST.Config.Database (queryDbSettings,
|
||||||
queryPgVersion,
|
queryPgVersion,
|
||||||
@@ -136,7 +138,7 @@ initPool AppConfig{..} =
|
|||||||
(fromIntegral configDbPoolAcquisitionTimeout)
|
(fromIntegral configDbPoolAcquisitionTimeout)
|
||||||
(fromIntegral configDbPoolMaxLifetime)
|
(fromIntegral configDbPoolMaxLifetime)
|
||||||
(fromIntegral configDbPoolMaxIdletime)
|
(fromIntegral configDbPoolMaxIdletime)
|
||||||
(toUtf8 configDbUri)
|
(toUtf8 $ addFallbackAppName prettyVersion configDbUri)
|
||||||
|
|
||||||
-- | Run an action with a database connection.
|
-- | Run an action with a database connection.
|
||||||
usePool :: AppState -> SQL.Session a -> IO (Either SQL.UsageError a)
|
usePool :: AppState -> SQL.Session a -> IO (Either SQL.UsageError a)
|
||||||
@@ -418,7 +420,7 @@ listener appState = do
|
|||||||
|
|
||||||
-- forkFinally allows to detect if the thread dies
|
-- forkFinally allows to detect if the thread dies
|
||||||
void . flip forkFinally (handleFinally dbChannel) $ do
|
void . flip forkFinally (handleFinally dbChannel) $ do
|
||||||
dbOrError <- acquire $ toUtf8 configDbUri
|
dbOrError <- acquire $ toUtf8 (addFallbackAppName prettyVersion configDbUri)
|
||||||
case dbOrError of
|
case dbOrError of
|
||||||
Right db -> do
|
Right db -> do
|
||||||
logWithZTime appState $ "Listening for notifications on the " <> dbChannel <> " channel"
|
logWithZTime appState $ "Listening for notifications on the " <> dbChannel <> " channel"
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ module PostgREST.Config
|
|||||||
, readPGRSTEnvironment
|
, readPGRSTEnvironment
|
||||||
, toURI
|
, toURI
|
||||||
, parseSecret
|
, parseSecret
|
||||||
|
, addFallbackAppName
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Crypto.JOSE.Types as JOSE
|
import qualified Crypto.JOSE.Types as JOSE
|
||||||
@@ -47,6 +48,9 @@ import Data.List (lookup)
|
|||||||
import Data.List.NonEmpty (fromList, toList)
|
import Data.List.NonEmpty (fromList, toList)
|
||||||
import Data.Maybe (fromJust)
|
import Data.Maybe (fromJust)
|
||||||
import Data.Scientific (floatingOrInteger)
|
import Data.Scientific (floatingOrInteger)
|
||||||
|
import Network.URI (escapeURIString,
|
||||||
|
isUnescapedInURIComponent, parseURI,
|
||||||
|
uriQuery)
|
||||||
import Numeric (readOct, showOct)
|
import Numeric (readOct, showOct)
|
||||||
import System.Environment (getEnvironment)
|
import System.Environment (getEnvironment)
|
||||||
import System.Posix.Types (FileMode)
|
import System.Posix.Types (FileMode)
|
||||||
@@ -460,3 +464,41 @@ type Environment = M.Map [Char] Text
|
|||||||
readPGRSTEnvironment :: IO Environment
|
readPGRSTEnvironment :: IO Environment
|
||||||
readPGRSTEnvironment =
|
readPGRSTEnvironment =
|
||||||
M.map T.pack . M.fromList . filter (isPrefixOf "PGRST_" . fst) <$> getEnvironment
|
M.map T.pack . M.fromList . filter (isPrefixOf "PGRST_" . fst) <$> getEnvironment
|
||||||
|
|
||||||
|
-- | Adds a `fallback_application_name` value to the connection string. This allows querying the PostgREST version on pg_stat_activity.
|
||||||
|
--
|
||||||
|
-- >>> let ver = "11.1.0 (5a04ec7)"::ByteString
|
||||||
|
-- >>> let strangeVer = "11'1&0@#$%,.:\"[]{}?+^()=asdfqwer"::ByteString
|
||||||
|
--
|
||||||
|
-- >>> addFallbackAppName ver "postgres://user:pass@host:5432/postgres"
|
||||||
|
-- "postgres://user:pass@host:5432/postgres?fallback_application_name=PostgREST%2011.1.0%20%285a04ec7%29"
|
||||||
|
--
|
||||||
|
-- >>> addFallbackAppName ver "postgres://user:pass@host:5432/postgres?"
|
||||||
|
-- "postgres://user:pass@host:5432/postgres?fallback_application_name=PostgREST%2011.1.0%20%285a04ec7%29"
|
||||||
|
--
|
||||||
|
-- >>> addFallbackAppName ver "postgres:///postgres?host=server&port=5432"
|
||||||
|
-- "postgres:///postgres?host=server&port=5432&fallback_application_name=PostgREST%2011.1.0%20%285a04ec7%29"
|
||||||
|
--
|
||||||
|
-- >>> addFallbackAppName ver "host=localhost port=5432 dbname=postgres"
|
||||||
|
-- "host=localhost port=5432 dbname=postgres fallback_application_name='PostgREST 11.1.0 (5a04ec7)'"
|
||||||
|
--
|
||||||
|
-- >>> addFallbackAppName ver "postgresql://"
|
||||||
|
-- "postgresql://?fallback_application_name=PostgREST%2011.1.0%20%285a04ec7%29"
|
||||||
|
--
|
||||||
|
-- >>> addFallbackAppName strangeVer "host=localhost port=5432 dbname=postgres"
|
||||||
|
-- "host=localhost port=5432 dbname=postgres fallback_application_name='PostgREST 11\\'1&0@#$%,.:\"[]{}?+^()=asdfqwer'"
|
||||||
|
--
|
||||||
|
-- >>> addFallbackAppName strangeVer "postgres:///postgres?host=server&port=5432"
|
||||||
|
-- "postgres:///postgres?host=server&port=5432&fallback_application_name=PostgREST%2011%271%260%40%23%24%25%2C.%3A%22%5B%5D%7B%7D%3F%2B%5E%28%29%3Dasdfqwer"
|
||||||
|
addFallbackAppName :: ByteString -> Text -> Text
|
||||||
|
addFallbackAppName version dbUri = dbUri <>
|
||||||
|
case uriQuery <$> parseURI (toS dbUri) of
|
||||||
|
Nothing -> " " <> keyValFmt -- Assume key/value connection string if the uri is not valid
|
||||||
|
Just "" -> "?" <> uriFmt
|
||||||
|
Just "?" -> uriFmt
|
||||||
|
_ -> "&" <> uriFmt
|
||||||
|
where
|
||||||
|
uriFmt = pKeyWord <> toS (escapeURIString isUnescapedInURIComponent $ toS pgrstVer)
|
||||||
|
keyValFmt = pKeyWord <> "'" <> T.replace "'" "\\'" pgrstVer <> "'"
|
||||||
|
pKeyWord = "fallback_application_name="
|
||||||
|
pgrstVer = "PostgREST " <> T.decodeUtf8 version
|
||||||
|
|||||||
@@ -17,4 +17,5 @@ main =
|
|||||||
, "src/PostgREST/ApiRequest/QueryParams.hs"
|
, "src/PostgREST/ApiRequest/QueryParams.hs"
|
||||||
, "src/PostgREST/Error.hs"
|
, "src/PostgREST/Error.hs"
|
||||||
, "src/PostgREST/MediaType.hs"
|
, "src/PostgREST/MediaType.hs"
|
||||||
|
, "src/PostgREST/Config.hs"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -158,3 +158,12 @@ create or replace function migrate_function() returns void as $_$
|
|||||||
$$ language sql;
|
$$ language sql;
|
||||||
notify pgrst, 'reload schema';
|
notify pgrst, 'reload schema';
|
||||||
$_$ language sql security definer;
|
$_$ language sql security definer;
|
||||||
|
|
||||||
|
create or replace function get_pgrst_version() returns text
|
||||||
|
language sql
|
||||||
|
as $$
|
||||||
|
select application_name
|
||||||
|
from pg_stat_activity
|
||||||
|
where application_name ilike 'postgrest%'
|
||||||
|
limit 1;
|
||||||
|
$$
|
||||||
|
|||||||
@@ -998,3 +998,44 @@ def test_openapi_in_big_schema(defaultenv):
|
|||||||
with run(env=env) as postgrest:
|
with run(env=env) as postgrest:
|
||||||
response = postgrest.session.get("/")
|
response = postgrest.session.get("/")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("dburi_type", ["no_params", "no_params_qmark", "with_params"])
|
||||||
|
def test_get_pgrst_version_with_uri_connection_string(dburi_type, dburi, defaultenv):
|
||||||
|
"The fallback_application_name should be added to the db-uri if it has a URI format"
|
||||||
|
defaultenv_without_libpq = {
|
||||||
|
key: value
|
||||||
|
for key, value in defaultenv.items()
|
||||||
|
if key not in ["PGDATABASE", "PGHOST", "PGUSER"]
|
||||||
|
}
|
||||||
|
|
||||||
|
env = {
|
||||||
|
"no_params": {**defaultenv, "PGRST_DB_URI": "postgresql://"},
|
||||||
|
"no_params_qmark": {**defaultenv, "PGRST_DB_URI": "postgresql://?"},
|
||||||
|
"with_params": {**defaultenv_without_libpq, "PGRST_DB_URI": dburi.decode()},
|
||||||
|
}
|
||||||
|
|
||||||
|
with run(env=env[dburi_type]) as postgrest:
|
||||||
|
response = postgrest.session.post("/rpc/get_pgrst_version")
|
||||||
|
version = '"%s"' % response.headers["Server"].replace(
|
||||||
|
"postgrest/", "PostgREST "
|
||||||
|
)
|
||||||
|
assert response.text == version
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_pgrst_version_with_keyval_connection_string(defaultenv):
|
||||||
|
"The fallback_application_name should be added to the db-uri if it has a keyword/value format"
|
||||||
|
uri = f'dbname={defaultenv["PGDATABASE"]} host={defaultenv["PGHOST"]} user={defaultenv["PGUSER"]}'
|
||||||
|
defaultenv_without_libpq = {
|
||||||
|
key: value
|
||||||
|
for key, value in defaultenv.items()
|
||||||
|
if key not in ["PGDATABASE", "PGHOST", "PGUSER"]
|
||||||
|
}
|
||||||
|
env = {**defaultenv_without_libpq, "PGRST_DB_URI": uri}
|
||||||
|
|
||||||
|
with run(env=env) as postgrest:
|
||||||
|
response = postgrest.session.post("/rpc/get_pgrst_version")
|
||||||
|
version = '"%s"' % response.headers["Server"].replace(
|
||||||
|
"postgrest/", "PostgREST "
|
||||||
|
)
|
||||||
|
assert response.text == version
|
||||||
|
|||||||
Reference in New Issue
Block a user