packages feed

a-piece-of-flake-0.0.1: src/PieceOfFlake/CmdArgs.hs

{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE TemplateHaskell #-}
module PieceOfFlake.CmdArgs where

import Data.Char (isDigit)
import Data.Either.Combinators ( mapLeft )
import GHC.TypeLits (KnownSymbol)
import Network.HostName ( getHostName )
import Options.Applicative
import PieceOfFlake.Flake ( FetcherId(..) )
import PieceOfFlake.Prelude
import PieceOfFlake.Prelude qualified as P
import PieceOfFlake.Req ( DynamicUrl(UrlHttp), http, port, parseUrl )
import PieceOfFlake.WebService
    ( FetcherHeartbeatPeriod, FetcherSecret, NoSubmitionHeartbeatSec )
import Text.Show ( Show(show) )

data HttpPort
data Cert
data CertKey
data AcidFlakesPath
data StaticCacheSeconds
data RawNixCacheOutput
data BaseUrl
data ResubmitPeriod
type IndexQueryCacheSize = "index-query-cache-size"
type BadFlakeMaxAge = "bad-flake-max-age"

newtype RingBufferSize = RingBufferSize (Refined (FromTo 1 12) Int)

instance Show RingBufferSize where
  show = P.show . unrefine . coerce


data WsCmdArgs
  = WsCmdArgs
    { httpPortToListen :: Tagged HttpPort Int
    , certFile :: Maybe (Tagged Cert FilePath)
    , keyFile :: Maybe (Tagged CertKey FilePath)
    , acidFlakes :: Tagged AcidFlakesPath FilePath
    , staticCache :: Tagged StaticCacheSeconds Word32
    , baseUrl :: Tagged BaseUrl Text
    , fetcherSecretPath :: Tagged FetcherSecret FilePath
    , noSubmitionHeartbeat :: Tagged NoSubmitionHeartbeatSec Second
    , allowResubmitBadFlakeIn :: Tagged ResubmitPeriod NominalDiffTime
    , logLevel :: LogLevel
    , indexQueryCacheSize :: Tagged IndexQueryCacheSize Word
    , ringBufferSize :: RingBufferSize
    , badFlakeMaxAge :: Tagged BadFlakeMaxAge Second
    , allowResubmitIndexedFlakeIn :: Tagged ResubmitPeriod NominalDiffTime
    , fetcherHeartbeatPeriod :: Tagged FetcherHeartbeatPeriod Second
    }
  deriving Show

type RawNixCacheMaxAge = "nix-cache-max-age"
type RawNixCacheErrorMaxAge = "nix-cache-err-max-age"
type LooseFlakes = "loose-flake"
type NixGCDiskCriticalCapacity = "nix-gc-used"
data FetcherCmdArgs
  = FetcherCmdArgs
    { webServiceUrl :: DynamicUrl
    , rawNixCache :: Tagged RawNixCacheOutput (Maybe FilePath)
    , fetcherId :: FetcherId
    , fetcherSecretPath :: Tagged FetcherSecret FilePath
    , rawNixCacheMaxAge :: Tagged RawNixCacheMaxAge NominalDiffTime
    , rawNixCacheErrMaxAge :: Tagged RawNixCacheErrorMaxAge NominalDiffTime
    , logLevel :: LogLevel
    , looseFlakes :: Tagged LooseFlakes Bool
    , runNixGCIfUsedMoreThan :: Tagged NixGCDiskCriticalCapacity Percent
    }
  deriving (Show)

data SubmitListOfFlakesArgs
  = SubmitListOfFlakesArgs
  { webServiceUrl :: DynamicUrl
  , logLevel :: LogLevel
  , indexTimeoutIn :: Tagged ResubmitPeriod NominalDiffTime
  }
  deriving (Show)

data CmdArgs
  = WebService WsCmdArgs
  | FetcherJob FetcherCmdArgs
  | SubmitListOfFlakes SubmitListOfFlakesArgs
  | PieceOfFlakeVersion
  deriving (Show)

execWithArgs :: MonadIO m => (CmdArgs -> m a) -> [String] -> m a
execWithArgs a args = a =<< liftIO (handleParseResult $ execParserPure defaultPrefs (info (cmdp <**> helper) phelp) args)
  where
    serviceP = WebService <$> (WsCmdArgs <$> portOption <*> certO <*>
      certKeyO <*> acidOption <*> cacheSecondsO <*>
      baseUrlO <*> fetcherSecretPathO <*> noSubmitionHeartbeatO <*>
      allowResubmitBadFlakeInO <*> logLevelO <*>
      indexQueryCacheSizeO <*> ringBufferSizeO <*> badFlakeMaxAgeO <*>
      allowResubmitIndexedFlakeInO <*> fetcherHeartbeatPeriodO)
    fetcherP = FetcherJob <$> (FetcherCmdArgs <$> urlOption <*> rawNixCacheO <*>
      customFetcherIdO <*> fetcherSecretPathO <*>
      rawNixCacheMaxAgeO @RawNixCacheMaxAge (7 * 24 * 3600) <*>
      rawNixCacheMaxAgeO @RawNixCacheErrorMaxAge 800 <*>
      logLevelO <*> looseFlakesO <*> nixGcDiskUsedO)
    submitListP = SubmitListOfFlakes <$>
      (SubmitListOfFlakesArgs <$> urlOption <*> logLevelO <*> allowResubmitBadFlakeInO)
    cmdp =
      hsubparser
        (  command "web" (infoP serviceP "launch web service")
        <> command "fetcher" (infoP fetcherP "launch fetcher job")
        <> command "submit-list" (infoP submitListP "submit list of flakes read for stdin - an url per line")
        <> command "version" (infoP (pure PieceOfFlakeVersion) "print program version"))

    infoP p h = info p (progDesc h <> fullDesc)
    phelp =
      progDesc
        "Nix Flake repository"

defaultPort :: Int
defaultPort = 3003

newtype Percent = Percent Word8 deriving newtype (Eq, Ord)
instance Show Percent where
  show (Percent p) = P.show p <> "%"

parsePercent :: String -> Maybe Percent
parsePercent s =
  case span isDigit s of
    (digits@(_:_), "%") -> Percent <$> readMaybe digits
    _bad -> Nothing

nixGcDiskUsedO :: Parser (Tagged NixGCDiskCriticalCapacity Percent)
nixGcDiskUsedO = Tagged <$>
  option (maybeReader parsePercent)
  ( long (symbolVal $ Proxy @NixGCDiskCriticalCapacity)
    <> showDefault
    <> value (Percent 90)
    <> help "percent of root volume usage when nix GC is triggerred before fetching a flake"
  )

allowResubmitBadFlakeInO :: Parser (Tagged ResubmitPeriod NominalDiffTime)
allowResubmitBadFlakeInO = Tagged <$>
  option auto
  ( long "resubmit-bad-flake-interval"
    <> showDefault
    <> value 600
    <> help "how soon bad flake can be resubmitted"
  )

allowResubmitIndexedFlakeInO :: Parser (Tagged ResubmitPeriod NominalDiffTime)
allowResubmitIndexedFlakeInO = Tagged <$>
  option auto
  ( long "resubmit-indexed-flake-interval"
    <> showDefault
    <> value (toNominal (2 :: Hour))
    <> help "how soon an indexed flake can be resubmitted"
  )

looseFlakesO :: Parser (Tagged LooseFlakes Bool)
looseFlakesO = Tagged <$>
  option auto
  ( long (symbolVal $ Proxy @LooseFlakes)
    <> showDefault
    <> value False
    <> help "loose fetched flake - debugging web service response"
  )

logLevelO :: Parser LogLevel
logLevelO =
  option auto
  ( long "log-level"
    <> short 'l'
    <> showDefault
    <> value LevelDebug
    <> help "app log level"
    <> metavar "LOG"
  )

rawNixCacheMaxAgeO :: forall a. KnownSymbol a => NominalDiffTime -> Parser (Tagged a NominalDiffTime)
rawNixCacheMaxAgeO defVal =
 Tagged <$>
  option auto
  ( long (symbolVal $ Proxy @a)
    <> showDefault
    <> value defVal
    <> help "max age of cached entity"
  )

badFlakeMaxAgeO :: Parser (Tagged BadFlakeMaxAge Second)
badFlakeMaxAgeO = Tagged <$>
  option auto
  ( long (symbolVal $ Proxy @BadFlakeMaxAge)
    <> showDefault
    <> value (8 * 3600)
    <> help "bad flake max age"
  )

fetcherHeartbeatPeriodO :: Parser (Tagged FetcherHeartbeatPeriod Second)
fetcherHeartbeatPeriodO = Tagged <$>
  option auto
  ( long "fetcher-heartbeat"
    <> showDefault
    <> value 10
    <> help "period of heartbeats from fetcher to make WS keep flake association with fetcher"
  )

noSubmitionHeartbeatO :: Parser (Tagged NoSubmitionHeartbeatSec Second)
noSubmitionHeartbeatO = Tagged <$>
  option auto
  ( long "heartbeat"
    <> short 'b'
    <> showDefault
    <> value 600
    <> help "period for putting empty submition request into fetcher queue (in seconds) when queue is empty"
    <> metavar "HEARTBEAT"
  )

ringBufferSizeO :: Parser RingBufferSize
ringBufferSizeO =
  option (eitherReader (mapLeft toString . readEither >=> fmap RingBufferSize . mapLeft P.show . refine))
  ( long "ring-buffer"
    <> showDefault
    <> value (RingBufferSize $$(refineTH 3))
    <> help "ring buffer size that  is used for mean values"
  )

indexQueryCacheSizeO :: Parser (Tagged IndexQueryCacheSize Word)
indexQueryCacheSizeO = Tagged <$>
  option auto
  ( long (symbolVal $ Proxy @IndexQueryCacheSize)
    <> showDefault
    <> value 100
    <> help "index query cache size"
  )

cacheSecondsO :: Parser (Tagged StaticCacheSeconds Word32)
cacheSecondsO = Tagged <$>
  option auto
  ( long "static-cache"
    <> showDefault
    <> value 1
    <> help "cache duration for static content (used in HTTP header)"
    <> metavar "STATIC_CACHE"
  )

rawNixCacheO :: Parser (Tagged RawNixCacheOutput (Maybe FilePath))
rawNixCacheO = Tagged <$>
  option (eitherReader parse )
  ( long "raw-nix-cache-output"
    <> short 'c'
    <> showDefault
    <> value (pure "fetcher-raw-nix-cache")
    <> help "path to fetcher cache of nix raw output"
    <> metavar "RAW_NIX_CACHE"
  )
  where
    parse = \case
      "" -> pure Nothing
      "null" -> pure Nothing
      "-" -> pure Nothing
      o -> pure $ Just o

customFetcherIdO :: Parser FetcherId
customFetcherIdO = FetcherId . toText <$>
  option str
  ( long "fetcher-id"
    <> short 'i'
    <> showDefault
    <> value (unsafePerformIO getHostName)
    <> help "fetcher id"
    <> metavar "FID"
  )

fetcherSecretPathO :: Parser (Tagged FetcherSecret FilePath)
fetcherSecretPathO = Tagged <$>
  option str
  ( long "fetcher-secret"
    <> short 's'
    <> showDefault
    <> value ".fetcher-secret"
    <> help "path to file with secret for fetcher authentication on web service"
    <> metavar "SECRET"
  )

acidOption :: Parser (Tagged AcidFlakesPath FilePath)
acidOption = Tagged <$>
  option str
  ( long "acid"
    <> short 'a'
    <> showDefault
    <> value  "acid-flakes/"
    <> help "path do ACID flake store"
    <> metavar "ACID"
  )

baseUrlO :: Parser (Tagged BaseUrl Text)
baseUrlO = Tagged <$>
  option str
  ( long "base-url"
    <> short 'u'
    <> showDefault
    <> value ("http://localhost:" <> P.show defaultPort)
    <> help "base web service url for HTML links"
    <> metavar "URL"
  )

urlOption :: Parser DynamicUrl
urlOption =
  option (eitherReader (mapLeft toString . parseUrl . toText))
  ( long "url"
    <> short 'u'
    <> showDefault
    <> value (UrlHttp (http "localhost") (port defaultPort))
    <> help "web service url for fetching flake submition requests and uploading flake meta back"
    <> metavar "URL"
  )

portOption :: Parser (Tagged HttpPort Int)
portOption = Tagged <$>
  option auto
  ( long "port"
    <> short 'p'
    <> showDefault
    <> value defaultPort
    <> help "HTTP(S) port to listen"
    <> metavar "PORT"
  )

emptyToNothing :: FilePath -> Maybe FilePath
emptyToNothing "" = Nothing
emptyToNothing s = Just s

zeroToNothing :: Int -> Maybe Int
zeroToNothing s | s <= 0 = Nothing
                | otherwise = Just s

pured :: (Applicative g, Applicative f) => g a -> g (f a)
pured = fmap pure

certO :: Parser (Maybe (Tagged Cert FilePath))
certO = pured . emptyToNothing <$>
  strOption
  ( long "certificate"
    <> short 'c'
    <> value ""
    <> help "path to SSL certificate file (./certificate.pem)"
    <> metavar "CERT"
  )
certKeyO :: Parser (Maybe (Tagged CertKey FilePath))
certKeyO = pured . emptyToNothing <$>
  strOption
  ( long "key"
    <> short 'k'
    <> value ""
    <> help "path to key file of SSL certificate (./key.pem)"
    <> metavar "KEY"
  )