module Config ( Config (..), MetricsConfig (..), StorageBackend (..), periodicSyncingEnabled, configInfo, ) where import Control.Applicative (optional) import Data.Semigroup ((<>)) import Options.Applicative import qualified Network.Wai.Handler.Warp as Warp import qualified Text.Read as Read import qualified Data.Char as Char import Data.Maybe (isJust) import Data.String (fromString) import qualified Data.List as List import qualified Data.Text as Text import qualified Web.JWT as JWT data StorageBackend = File | Sqlite -- command-line arguments data Config = Config { configDataFile :: Maybe FilePath , configPort :: Int -- | Enables the use of JWT for authorization in JWT. , configEnableJwtAuth :: Bool -- | The secret used for verifying the JWT signatures. If no secret is -- specified even though JWT authorization is enabled, tokens will still be -- used, but not be verified. , configJwtSecret :: Maybe JWT.Signer , configMetricsEndpoint :: Maybe MetricsConfig , configQueueCapacity :: Word , configSyncIntervalMicroSeconds :: Maybe Int -- | Enable journaling, only in conjunction with periodic syncing , configEnableJournaling :: Bool -- | Indicates that the sentry logging is disabled, can be used to overwrite -- ```configSentryDSN``` or the environment variable , configDisableSentryLogging :: Bool -- | The SENTRY_DSN key that Sentry uses to communicate, if not set, use Nothing. -- Just indicates that a key is given. , configSentryDSN :: Maybe String , configStorageBackend :: StorageBackend } data MetricsConfig = MetricsConfig { metricsConfigHost :: Warp.HostPreference , metricsConfigPort :: Warp.Port } periodicSyncingEnabled :: Config -> Bool periodicSyncingEnabled = isJust . configSyncIntervalMicroSeconds -- Parsing of command-line arguments type EnvironmentConfig = [(String, String)] configParser :: EnvironmentConfig -> Parser Config configParser environment = Config -- Note: If no --data-file is given we default either to icepeak.json or icepeak.db <$> optional (strOption (long "data-file" <> metavar "DATA_FILE" <> help "File where data is persisted to. Default: icepeak.json")) <*> option auto (long "port" <> metavar "PORT" <> maybe (value 3000) value (readFromEnvironment "ICEPEAK_PORT") <> help "Port to listen on, defaults to the value of the ICEPEAK_PORT environment variable if present, or 3000 if not") <*> switch (long "enable-jwt-auth" <> help "Enable authorization using JSON Web Tokens.") <*> optional (secretOption ( long "jwt-secret" <> metavar "JWT_SECRET" <> environ "JWT_SECRET" <> help "Secret used for JWT verification, defaults to the value of the JWT_SECRET environment variable if present. If no secret is passed, JWT tokens are not checked for validity.")) <*> optional (option metricsConfigReader (long "metrics" <> metavar "HOST:PORT" <> help "If provided, Icepeak collects various metrics and provides them to Prometheus on the given endpoint." )) <*> option auto (long "queue-capacity" <> metavar "INTEGER" <> value 256 <> help ("Smaller values decrease the risk of data loss during a crash, while " <> "higher values result in more requests being accepted in rapid succession.")) <*> optional (option timeDurationReader (long "sync-interval" <> metavar "DURATION" <> help ("If supplied, data is only persisted to disc every DURATION time units." <> "The units 'm' (minutes), 's' (seconds) and 'ms' (milliseconds) can be used. " <> "When omitting this argument, data is persisted after every modification"))) <*> switch (long "journaling" <> help "Enable journaling. This only has an effect when periodic syncing is enabled.") <*> switch (long "disable-sentry-logging" <> help "Disable error logging via Sentry") <*> optional (strOption ( long "sentry-dsn" <> metavar "SENTRY_DSN" <> environ "SENTRY_DSN" <> help "Sentry DSN used for Sentry logging, defaults to the value of the SENTRY_DSN environment variable if present. If no secret is passed, Sentry logging will be disabled.")) <*> storageBackend where environ var = foldMap value (lookup var environment) readFromEnvironment :: Read a => String -> Maybe a readFromEnvironment var = lookup var environment >>= Read.readMaybe secretOption m = JWT.hmacSecret . Text.pack <$> strOption m configInfo :: EnvironmentConfig -> ParserInfo Config configInfo environment = info parser description where parser = helper <*> configParser environment description = fullDesc <> header "Icepeak - Fast Json document store with push notification support." -- * Parsers storageBackend :: Parser StorageBackend storageBackend = fileBackend <|> sqliteBackend fileBackend :: Parser StorageBackend -- The first 'File' here is the default value. We want --file to be used by default, when nothing -- is specified on the command-line. This ensures backwards-compatibility. fileBackend = flag File File (long "file" <> help "Use a file as the storage backend." ) sqliteBackend :: Parser StorageBackend sqliteBackend = flag' Sqlite (long "sqlite" <> help "Use a sqlite file as the storage backend." ) -- * Reader functions metricsConfigReader :: ReadM MetricsConfig metricsConfigReader = eitherReader $ \input -> case List.break (== ':') input of (hostStr, ':':portStr) -> MetricsConfig (fromString hostStr) <$> Read.readEither portStr (_, _) -> Left "no port specified" -- | Read an option as a time duration in microseconds. timeDurationReader :: ReadM Int timeDurationReader = eitherReader $ \input -> case List.break Char.isLetter input of ("", _) -> Left "no amount specified" (amount, unit) -> case lookup unit units of Nothing -> Left "invalid unit" Just factor -> fmap (* factor) $ Read.readEither amount where -- defines the available units and how they convert to microseconds units = [ ("s", 1000000) , ("ms", 1000) , ("m", 60000000) ]