cachix-1.0.1: src/Cachix/Client/Config.hs
{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE QuasiQuotes #-}
module Cachix.Client.Config
( -- CLI options
run,
parser,
Command (..),
CachixOptions (..),
-- Auth token helpers
getAuthTokenRequired,
getAuthTokenMaybe,
setAuthToken,
noAuthTokenError,
-- Config
getConfig,
readConfig,
writeConfig,
getDefaultFilename,
ConfigPath,
Config (..),
BinaryCacheConfig (..),
setBinaryCaches,
)
where
import Cachix.Client.Config.Orphans ()
import Cachix.Client.Exception (CachixException (..))
import Cachix.Client.URI as URI
import qualified Control.Exception.Safe as Safe
import Data.Either.Extra (eitherToMaybe)
import Data.String.Here
import qualified Dhall
import qualified Dhall.Pretty
import qualified Options.Applicative as Opt
import qualified Prettyprinter as Pretty
import qualified Prettyprinter.Render.Text as Pretty
import Protolude hiding (toS)
import Protolude.Conv
import Servant.Auth.Client
import System.Directory
( XdgDirectory (..),
createDirectoryIfMissing,
getXdgDirectory,
)
import System.Environment (lookupEnv)
import System.FilePath.Posix (takeDirectory)
import System.Posix.Files
( ownerReadMode,
ownerWriteMode,
setFileMode,
unionFileModes,
)
import qualified URI.ByteString as URI
data CachixOptions = CachixOptions
{ host :: URI.URIRef URI.Absolute,
configPath :: ConfigPath,
verbose :: Bool
}
deriving (Show)
data Config = Config
{ authToken :: Token,
hostname :: URI.URIRef URI.Absolute,
binaryCaches :: [BinaryCacheConfig]
}
deriving (Show, Generic, Dhall.ToDhall, Dhall.FromDhall)
data BinaryCacheConfig = BinaryCacheConfig
{ name :: Text,
secretKey :: Text
}
deriving (Show, Generic, Dhall.FromDhall, Dhall.ToDhall)
defaultConfig :: Config
defaultConfig =
Config
{ authToken = Token "",
hostname = URI.defaultCachixURI,
binaryCaches = []
}
mergeWithDefault :: Text -> Text
mergeWithDefault config =
serializeConfig defaultConfig <> " // " <> config
type ConfigPath = FilePath
getConfig :: ConfigPath -> IO Config
getConfig filename = do
userConfig <- readConfig filename
pure $ fromMaybe defaultConfig userConfig
readConfig :: ConfigPath -> IO (Maybe Config)
readConfig filename = fmap eitherToMaybe . Safe.tryIO $ do
userConfig <- readFile filename
let config = mergeWithDefault userConfig
Dhall.detailed (Dhall.input Dhall.auto config)
getDefaultFilename :: IO FilePath
getDefaultFilename = do
dir <- getXdgDirectory XdgConfig "cachix"
return $ dir <> "/cachix.dhall"
serializeConfig :: Config -> Text
serializeConfig =
Pretty.renderStrict
. Pretty.layoutPretty Pretty.defaultLayoutOptions
. Dhall.Pretty.prettyExpr
. Dhall.embed Dhall.inject
writeConfig :: ConfigPath -> Config -> IO ()
writeConfig filename config = do
let doc = Dhall.Pretty.prettyExpr $ Dhall.embed Dhall.inject config
createDirectoryIfMissing True (takeDirectory filename)
writeFile filename $ show doc
assureFilePermissions filename
putStrLn $ "Written to " <> filename
-- chmod rw filepath
assureFilePermissions :: FilePath -> IO ()
assureFilePermissions fp =
setFileMode fp $ unionFileModes ownerReadMode ownerWriteMode
getAuthTokenFromConfig :: Config -> Maybe Token
getAuthTokenFromConfig = inspectToken . authToken
where
inspectToken (Token "") = Nothing
inspectToken token = Just token
getAuthTokenRequired :: Config -> IO Token
getAuthTokenRequired config = do
authTokenMaybe <- getAuthTokenMaybe config
case authTokenMaybe of
Just authtoken -> return authtoken
Nothing -> throwIO $ NoConfig $ toS noAuthTokenError
-- get auth token from env variable or fallback to config
getAuthTokenMaybe :: Config -> IO (Maybe Token)
getAuthTokenMaybe config = do
maybeAuthToken <- lookupEnv "CACHIX_AUTH_TOKEN"
case maybeAuthToken of
Just token -> return $ Just $ Token (toS token)
Nothing -> return $ getAuthTokenFromConfig config
noAuthTokenError :: Text
noAuthTokenError =
[iTrim|
Start by visiting https://app.cachix.org and create a personal/cache auth token.
To configure the token:
a) Via environment variable:
$ export CACHIX_AUTH_TOKEN=<token...>
b) Via configuration file:
$ cachix authtoken <token...>
|]
-- Setters
setAuthToken :: Token -> Config -> Config
setAuthToken token config = config {authToken = token}
setBinaryCaches :: [BinaryCacheConfig] -> Config -> Config
setBinaryCaches caches config = config {binaryCaches = binaryCaches config <> caches}
-- CLI parsers
data Command
= Get GetCommand
| Set SetCommand
deriving (Show)
data GetCommand
= GetHostname
deriving (Show)
data SetCommand
= SetHostname (URI.URIRef URI.Absolute)
deriving (Show)
run :: CachixOptions -> Command -> IO ()
run CachixOptions {configPath} (Get cmd) = getConfigOption configPath cmd
run CachixOptions {configPath} (Set cmd) = setConfigOption configPath cmd
getConfigOption :: ConfigPath -> GetCommand -> IO ()
getConfigOption configPath cmd = do
config <- getConfig configPath
case cmd of
GetHostname -> putStrLn $ URI.serializeURIRef' (hostname config)
setConfigOption :: ConfigPath -> SetCommand -> IO ()
setConfigOption configPath (SetHostname hostname) = do
config <- getConfig configPath
writeConfig configPath config {hostname}
parser :: Opt.ParserInfo Command
parser =
Opt.info (Opt.helper <*> commandParser) $
Opt.progDesc "Manage configuration settings for cachix"
commandParser :: Opt.Parser Command
commandParser =
Opt.subparser $
mconcat
[ Opt.command "get" $
Opt.info (Opt.helper <*> getConfigOptionParser) $
Opt.progDesc "Get a configuration option",
Opt.command "set" $
Opt.info (Opt.helper <*> setConfigOptionParser) $
Opt.progDesc "Set a configuration option"
]
getConfigOptionParser :: Opt.Parser Command
getConfigOptionParser =
Opt.subparser $ Opt.metavar "KEY" <> mconcat (supportedOptions False)
setConfigOptionParser :: Opt.Parser Command
setConfigOptionParser =
Opt.subparser $ Opt.metavar "KEY VALUE" <> mconcat (supportedOptions True)
supportedOptions :: Bool -> [Opt.Mod Opt.CommandFields Command]
supportedOptions canModify =
[ Opt.command "hostname" $
Opt.info (Opt.helper <*> if canModify then fmap Set hostnameParser else pure (Get GetHostname)) $
Opt.progDesc "The hostname for the Cachix Deploy service."
]
hostnameParser :: Opt.Parser SetCommand
hostnameParser = do
SetHostname
<$> Opt.argument uriOption (Opt.metavar "HOSTNAME")
uriOption :: Opt.ReadM (URI.URIRef URI.Absolute)
uriOption = Opt.eitherReader $ \s ->
first show $ URI.parseURI URI.strictURIParserOptions (toS s)