cachix-1.7.5: src/Cachix/Client/Config.hs
{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE QuasiQuotes #-}
module Cachix.Client.Config
( -- CLI options
run,
parser,
Command (..),
CachixOptions (..),
defaultCachixOptions,
-- 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 Control.Exception.Safe qualified as Safe
import Data.Either.Extra (eitherToMaybe)
import Data.String.Here
import Dhall qualified
import Dhall.Pretty qualified
import Options.Applicative qualified as Opt
import Prettyprinter qualified as Pretty
import Prettyprinter.Render.Text qualified 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,
)
data CachixOptions = CachixOptions
{ host :: URI,
configPath :: ConfigPath,
verbose :: Bool
}
deriving (Show)
defaultCachixOptions :: IO CachixOptions
defaultCachixOptions = do
configPath <- getDefaultFilename
return $
CachixOptions
{ host = URI.defaultCachixURI,
configPath = configPath,
verbose = False
}
data Config = Config
{ authToken :: Token,
hostname :: URI,
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
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.serialize (hostname config) :: Text)
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
uriOption = Opt.eitherReader $ \s ->
first show $ URI.parseURI (toS s)