diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e4f76701..90761ee97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - HTTP 401 rather than 400 for expired JWT - @begriffs - Remove default JWT secret - @begriffs - Use GUC request.jwt.claim.foo rather than postgrest.claims.foo - @begriffs +- Use config file rather than command line arguments - @begriffs ## [0.3.2.0] - 2016-06-10 diff --git a/Dockerfile b/Dockerfile index a87861337..63022e899 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,6 @@ FROM debian:jessie -ENV POSTGREST_VERSION 0.3.2.0 -ENV POSTGREST_SCHEMA public -ENV POSTGREST_ANONYMOUS postgres -ENV POSTGREST_JWT_SECRET thisisnotarealsecret -ENV POSTGREST_MAX_ROWS 1000000 -ENV POSTGREST_POOL 200 +ENV POSTGREST_VERSION 0.4.0.0 RUN apt-get update && \ apt-get install -y tar xz-utils wget libpq-dev && \ @@ -16,12 +11,8 @@ RUN wget http://github.com/begriffs/postgrest/releases/download/v${POSTGREST_VER mv postgrest /usr/local/bin/postgrest && \ rm postgrest-${POSTGREST_VERSION}-ubuntu.tar.xz -CMD exec postgrest postgres://${PG_ENV_POSTGRES_USER}:${PG_ENV_POSTGRES_PASSWORD}@${PG_PORT_5432_TCP_ADDR}:${PG_PORT_5432_TCP_PORT}/${PG_ENV_POSTGRES_DB} \ - --port 3000 \ - --schema ${POSTGREST_SCHEMA} \ - --anonymous ${POSTGREST_ANONYMOUS} \ - --pool ${POSTGREST_POOL} \ - --jwt-secret ${POSTGREST_JWT_SECRET} \ - --max-rows ${POSTGREST_MAX_ROWS} +# PostgREST reads /etc/postgrest.conf so map the configuration +# file in when you run this container +CMD exec postgrest EXPOSE 3000 diff --git a/main/Main.hs b/main/Main.hs index f580ffb61..9140a33db 100644 --- a/main/Main.hs +++ b/main/Main.hs @@ -8,12 +8,14 @@ import PostgREST.Config (AppConfig (..), minimumPgVersion, prettyVersion, readOptions) +import PostgREST.Error (prettyUsageError) import PostgREST.OpenAPI (isMalformedProxyUri) import PostgREST.DbStructure import Control.AutoUpdate import Data.String (IsString (..)) import Data.Text (stripPrefix) +import Data.Text.IO (hPutStrLn) import Data.Function (id) import Data.Time.Clock.POSIX (getPOSIXTime) import qualified Hasql.Query as H @@ -68,6 +70,10 @@ main = do <> show minimumPgVersion) getDbStructure (toS $ configSchema conf) + forM_ (lefts [result]) $ \e -> do + hPutStrLn stderr (prettyUsageError e) + exitFailure + refDbStructure <- newIORef $ either (panic . show) id result #ifndef mingw32_HOST_OS diff --git a/postgrest.cabal b/postgrest.cabal index 2c9346c1a..93ee8a633 100644 --- a/postgrest.cabal +++ b/postgrest.cabal @@ -51,11 +51,13 @@ library , bytestring , case-insensitive , cassava + , configurator , containers , contravariant , hasql , hasql-pool == 0.4.1 , hasql-transaction == 0.4.5.1 + , heredoc , HTTP , http-types , insert-ordered-containers diff --git a/src/PostgREST/Config.hs b/src/PostgREST/Config.hs index eee8b9a34..c6beec133 100644 --- a/src/PostgREST/Config.hs +++ b/src/PostgREST/Config.hs @@ -2,12 +2,13 @@ Module : PostgREST.Config Description : Manages PostgREST configuration options. -This module provides a helper function to read the command line arguments using the optparse-applicative -and the AppConfig type to store them. -It also can be used to define other middleware configuration that may be delegated to some sort of -external configuration. +This module provides a helper function to read the command line +arguments using the optparse-applicative and the AppConfig type to store +them. It also can be used to define other middleware configuration that +may be delegated to some sort of external configuration. -It currently includes a hardcoded CORS policy but this could easly be turned in configurable behaviour if needed. +It currently includes a hardcoded CORS policy but this could easly be +turned in configurable behaviour if needed. Other hardcoded options such as the minimum version number also belong here. -} @@ -19,20 +20,26 @@ module PostgREST.Config ( prettyVersion ) where +import System.IO.Error (IOError) +import Control.Applicative import qualified Data.ByteString.Char8 as BS import qualified Data.CaseInsensitive as CI +import qualified Data.Configurator as C +import qualified Data.Configurator.Types as C import Data.List (lookup) import Data.Text (strip, intercalate) +import Data.Text.IO (hPutStrLn) import Data.Version (versionBranch) import Network.Wai import Network.Wai.Middleware.Cors (CorsResourcePolicy (..)) -import Options.Applicative +import Options.Applicative hiding (str) import Paths_postgrest (version) +import Text.Heredoc + import Protolude hiding (intercalate , (<>)) -import Safe (readMay) --- | Data type to store all command line options +-- | Config file settings for the server data AppConfig = AppConfig { configDatabase :: Text , configAnonRole :: Text @@ -47,20 +54,6 @@ data AppConfig = AppConfig { , configQuiet :: Bool } -argParser :: Parser AppConfig -argParser = AppConfig - <$> (toS <$> argument str (help "(REQUIRED) database connection string, e.g. postgres://user:pass@host:port/db" <> metavar "DB_URL")) - <*> (toS <$> strOption (long "anonymous" <> short 'a' <> help "(REQUIRED) postgres role to use for non-authenticated requests" <> metavar "ROLE")) - <*> (optional . map toS . strOption) (long "proxy-uri" <> short 'x' <> help "proxy uri of the HTTP server" <> metavar "PROXY") - <*> (toS <$> strOption (long "schema" <> short 's' <> help "schema to use for API routes" <> metavar "NAME" <> value "public" <> showDefault)) - <*> (toS <$> strOption (long "host" <> short 'l' <> help "hostname or ip on which to run HTTP server" <> metavar "HOST" <> value "*4" <> showDefault)) - <*> option auto (long "port" <> short 'p' <> help "port number on which to run HTTP server" <> metavar "PORT" <> value 3000 <> showDefault) - <*> (optional . map toS <$> strOption) (long "jwt-secret" <> short 'j' <> help "secret used to encrypt and decrypt JWT tokens" <> metavar "SECRET") - <*> option auto (long "pool" <> short 'o' <> help "max connections in database pool" <> metavar "COUNT" <> value 10 <> showDefault) - <*> (readMay <$> strOption (long "max-rows" <> short 'm' <> help "max rows in response" <> metavar "COUNT" <> value "infinity" <> showDefault)) - <*> (optional . map toS . strOption) (long "pre-request" <> help "schema-qualified name of proc to call to validate requests" <> metavar "FUNCTION") - <*> pure False - defaultCorsPolicy :: CorsResourcePolicy defaultCorsPolicy = CorsResourcePolicy Nothing ["GET", "POST", "PATCH", "DELETE", "OPTIONS"] ["Authorization"] Nothing @@ -90,16 +83,93 @@ prettyVersion = intercalate "." $ map show $ versionBranch version -- | Function to read and parse options from the command line readOptions :: IO AppConfig -readOptions = customExecParser parserPrefs opts - where - opts = info (helper <*> argParser) $ - fullDesc - <> progDesc ( - "PostgREST " - <> toS prettyVersion - <> " / create a REST API to an existing Postgres database" - ) - parserPrefs = prefs showHelpOnError +readOptions = do + args <- customExecParser parserPrefs opts + + when (caExample args) $ do + putStrLn ( + [str|db-uri = "postgres://user:pass@localhost:5432/dbname" + |db-schema = "public" + |db-anon-role = "postgres" + |db-pool = 10 + | + |server-host = "*4" + |server-port = 3000 + | + |## base url for swagger output + |# server-proxy-uri = "" + | + |## choose a secret to enable JWT auth + |## (use "@filename" to load from separate file) + |# jwt-secret = "foo" + | + |## limit rows in response + |# max-rows = 1000 + | + |## stored proc to exec immediately after auth + |# pre-request = "stored_proc_name" + |]::Text) + exitSuccess + + conf <- catch + (C.load [C.Required $ caConfig args]) + configNotfoundHint + + handle missingKeyHint $ do + -- db ---------------- + cDbUri <- C.require conf "db-uri" + cDbSchema <- C.require conf "db-schema" + cDbAnon <- C.require conf "db-anon-role" + cPool <- C.lookupDefault 10 conf "db-pool" + -- server ------------ + cHost <- C.lookupDefault "*4" conf "server-host" + cPort <- C.lookupDefault 3000 conf "server-port" + cProxy <- C.lookup conf "server-proxy-uri" + -- jwt --------------- + cJwtSec <- C.lookup conf "jwt-secret" + -- safety ------------ + cMaxRows <- C.lookup conf "max-rows" + cReqCheck <- C.lookup conf "pre-request" + + return $ AppConfig cDbUri cDbAnon cProxy cDbSchema cHost cPort + cJwtSec cPool cMaxRows cReqCheck False + + where + opts = info (helper <*> argParser) $ + fullDesc + <> progDesc ( + "PostgREST " + <> toS prettyVersion + <> " / create a REST API to an existing Postgres database" + ) + parserPrefs = prefs showHelpOnError + + configNotfoundHint :: IOError -> IO a + configNotfoundHint e = do + hPutStrLn stderr $ intercalate "\n" [ + "Cannot open config file:", + "\t" <> show e, + "\nUse the --help flag to learn how to fix this."] + exitFailure + + missingKeyHint :: C.KeyError -> IO a + missingKeyHint (C.KeyError n) = do + hPutStrLn stderr $ + "Required config parameter \"" <> n <> "\" is missing or of wrong type.\n" <> + "Try the --example-config option to see how to configure PostgREST." + exitFailure + +data CmdArgs = CmdArgs { + caConfig :: FilePath + , caExample :: Bool + } + +argParser :: Parser CmdArgs +argParser = CmdArgs <$> + (toS <$> strOption + (short 'c' <> metavar "filename" <> + help "Path to configuration file")) <*> + switch (long "example-config" <> help "output an example config file") -- | Tells the minimum PostgreSQL version required by this version of PostgREST minimumPgVersion :: Integer diff --git a/src/PostgREST/Error.hs b/src/PostgREST/Error.hs index 022b61458..84cbb14e2 100644 --- a/src/PostgREST/Error.hs +++ b/src/PostgREST/Error.hs @@ -2,7 +2,7 @@ {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE TypeSynonymInstances #-} -module PostgREST.Error (pgErrResponse, errResponse) where +module PostgREST.Error (pgErrResponse, errResponse, prettyUsageError) where import Protolude import Data.Aeson ((.=)) @@ -29,6 +29,11 @@ pgErrResponse authed e = else [jsonType] in responseLBS status hdrs (JSON.encode e) +prettyUsageError :: P.UsageError -> Text +prettyUsageError (P.ConnectionError e) = + "Database connection error:\n" <> toS (fromMaybe "" e) +prettyUsageError e = show $ JSON.encode e + instance JSON.ToJSON P.UsageError where toJSON (P.ConnectionError e) = JSON.object [ "code" .= ("" :: Text),