packages feed

cachix 0.3.8 → 0.5.0

raw patch · 13 files changed

+308/−235 lines, 13 filesdep ~servantdep ~servant-clientdep ~servant-client-core

Dependency ranges changed: servant, servant-client, servant-client-core

Files

CHANGELOG.md view
@@ -7,6 +7,19 @@  ## Unreleased +## [0.5.0] - 2020-11-06++### Added++- Allow specifying output directory to write nix.conf and netrc files. +- Allow pushing without a Signing key using only auth token+- Allow setting auth token via `$CACHIX_AUTH_TOKEN` shell variable++### Fixed++- Watch store command now pushes the full closure of each store path+- Support groups when parsing trusted-users from nix.conf+ ## [0.3.8] - 2020-06-03  ### Added@@ -210,7 +223,8 @@  - Initial release @domenkozar -[Unreleased]: https://github.com/cachix/cachix/compare/v0.3.7...HEAD+[Unreleased]: https://github.com/cachix/cachix/compare/v0.3.8...HEAD+[0.3.8]: https://github.com/cachix/cachix/compare/v0.3.7...v0.3.8 [0.3.7]: https://github.com/cachix/cachix/compare/v0.3.6...v0.3.7 [0.3.6]: https://github.com/cachix/cachix/compare/v0.3.5...v0.3.6 [0.3.5]: https://github.com/cachix/cachix/compare/v0.3.4...v0.3.5
cachix.cabal view
@@ -1,6 +1,6 @@ cabal-version:      2.2 name:               cachix-version: 0.3.8+version: 0.5.0 license:            Apache-2.0 license-file:       LICENSE copyright:          2018 Domen Kožar@@ -106,11 +106,11 @@     , resourcet     , retry     , safe-exceptions-    , servant               >=0.15+    , servant               >=0.16     , servant-auth     , servant-auth-client   >=0.3.3.0-    , servant-client        >=0.15-    , servant-client-core   >=0.15+    , servant-client        >=0.16+    , servant-client-core   >=0.16     , servant-conduit     , text     , unix
src/Cachix/Client/Commands.hs view
@@ -12,8 +12,8 @@   ) where -import qualified Cachix.Api as Api-import Cachix.Api.Error+import qualified Cachix.API as API+import Cachix.API.Error import Cachix.Client.Config   ( BinaryCacheConfig (BinaryCacheConfig),     Config (..),@@ -36,9 +36,9 @@ import Cachix.Client.Secrets   ( SigningKey (SigningKey),     exportSigningKey,-    parseSigningKeyLenient,   ) import Cachix.Client.Servant+import Cachix.Client.Store (Store) import qualified Cachix.Types.SigningKeyCreate as SigningKeyCreate import Control.Exception.Safe (handle, throwM) import Control.Retry (RetryStatus (rsIterNumber))@@ -54,7 +54,6 @@ import Servant.Client.Streaming import Servant.Conduit () import System.Directory (doesFileExist)-import System.Environment (lookupEnv) import System.FSNotify import System.IO (hIsTerminalDevice) @@ -62,16 +61,16 @@ authtoken env token = do   -- TODO: check that token actually authenticates!   writeConfig (configPath (cachixoptions env)) $ case config env of-    Just cfg -> cfg {authToken = Token (toS token)}+    Just cfg -> Config.setAuthToken cfg $ Token (toS token)     Nothing -> mkConfig token  create :: Env -> Text -> IO () create _ _ =-  throwIO $ DeprecatedCommand "Create command has been deprecated. Please visit https://cachix.org to create a binary cache."+  throwIO $ DeprecatedCommand "Create command has been deprecated. Please visit https://app.cachix.org to create a binary cache."  generateKeypair :: Env -> Text -> IO ()-generateKeypair Env {config = Nothing} _ = throwIO $ NoConfig "Start with visiting https://cachix.org and copying the token to $ cachix authtoken <token>"-generateKeypair env@Env {config = Just cfg} name = do+generateKeypair env name = do+  cachixAuthToken <- Config.getAuthTokenRequired (config env)   (PublicKey pk, sk) <- createKeypair   let signingKey = exportSigningKey $ SigningKey sk       signingKeyCreate = SigningKeyCreate.SigningKeyCreate (toS $ B64.encode pk)@@ -79,9 +78,12 @@   -- we first validate if key can be added to the binary cache   (_ :: NoContent) <-     escalate <=< (`runClientM` clientenv env) $-      Api.createKey (cachixBCClient name) (authToken cfg) signingKeyCreate+      API.createKey cachixClient cachixAuthToken name signingKeyCreate   -- if key was successfully added, write it to the config   -- TODO: warn if binary cache with the same key already exists+  let cfg = case config env of+        Just it -> it+        Nothing -> Config.mkConfig $ toS $ getToken cachixAuthToken   writeConfig (configPath (cachixoptions env)) $     cfg {binaryCaches = binaryCaches cfg <> [bcc]}   putStrLn@@ -105,26 +107,24 @@         Text     ) -envToToken :: Env -> Token-envToToken env =-  maybe (Token "") authToken (config env)--notAuthenticatedBinaryCache :: CachixException-notAuthenticatedBinaryCache =-  AccessDeniedBinaryCache "You must first authenticate using:  $ cachix authtoken <token>"+notAuthenticatedBinaryCache :: Text -> CachixException+notAuthenticatedBinaryCache name =+  AccessDeniedBinaryCache $+    "Binary cache " <> name <> " doesn't exist or it's private. " <> Config.noAuthTokenError  accessDeniedBinaryCache :: Text -> CachixException accessDeniedBinaryCache name =-  AccessDeniedBinaryCache $ "You don't seem to have API access to binary cache " <> name+  AccessDeniedBinaryCache $ "Binary cache " <> name <> " doesn't exist or it's private and you don't have access it"  use :: Env -> Text -> InstallationMode.UseOptions -> IO () use env name useOptions = do+  cachixAuthToken <- Config.getAuthTokenOptional (config env)   -- 1. get cache public key-  res <- (`runClientM` clientenv env) $ Api.get (cachixBCClient name) (envToToken env)+  res <- (`runClientM` clientenv env) $ API.getCache cachixClient cachixAuthToken name   case res of     Left err       | isErr err status401 && isJust (config env) -> throwM $ accessDeniedBinaryCache name-      | isErr err status401 -> throwM notAuthenticatedBinaryCache+      | isErr err status401 -> throwM $ notAuthenticatedBinaryCache name       | isErr err status404 -> throwM $ BinaryCacheNotFound $ "Binary cache" <> name <> " does not exist."       | otherwise -> throwM err     Right binaryCache -> do@@ -140,7 +140,7 @@                 InstallationMode.isNixOS = isNixOS               }       InstallationMode.addBinaryCache (config env) binaryCache useOptions $-        fromMaybe (InstallationMode.getInstallationMode nixEnv) (InstallationMode.useMode useOptions)+        InstallationMode.getInstallationMode nixEnv useOptions  -- TODO: lots of room for performance improvements push :: Env -> PushArguments -> IO ()@@ -156,7 +156,7 @@       -- that may or may not be written to by the caller.       -- This is somewhat like the behavior of `cat` for example.       (_, paths) -> return paths-  sk <- findSigningKey env name+  cacheSecret <- findPushSecret (config env) name   store <- wait (storeAsync env)   void $     pushClosure@@ -164,28 +164,37 @@       (clientenv env)       store       PushCache-        { pushCacheToken = envToToken env,-          pushCacheName = name,-          pushCacheSigningKey = sk+        { pushCacheName = name,+          pushCacheSecret = cacheSecret         }       (pushStrategy env opts name)       inputStorePaths   putText "All done." push env (PushWatchStore opts name) = withManager $ \mgr -> do-  _ <- watchDir mgr "/nix/store" filterF action-  _ <- wait (storeAsync env)-  putText "Watching /nix/store for new builds ..."-  forever $ threadDelay 1000000+  store <- wait (storeAsync env)+  pushSecret <- findPushSecret (config env) name+  _ <- watchDir mgr "/nix/store" filterF (action store pushSecret)+  putText "Watching /nix/store for new paths ..."+  forever $ threadDelay (1000 * 1000)   where+    action :: Store -> PushSecret -> Action+    action store pushSecret (Removed fp _ _) =+      let storePath = toS $ dropEnd 5 fp+       in Control.Exception.Safe.handle (logErr fp) $ void $+            pushClosure+              (mapConcurrentlyBounded (numJobs opts))+              (clientenv env)+              store+              ( PushCache+                  { pushCacheName = name,+                    pushCacheSecret = pushSecret+                  }+              )+              (pushStrategy env opts name)+              [storePath]+    action _ _ _ = return ()     logErr :: FilePath -> SomeException -> IO ()     logErr fp e = hPutStrLn stderr $ "Exception occured while pushing " <> fp <> ": " <> show e-    action :: Action-    action (Removed fp _ _) =-      Control.Exception.Safe.handle (logErr fp)-        $ pushStorePath env opts name-        $ toS-        $ dropEnd 5 fp-    action _ = return ()     filterF :: ActionPredicate     filterF (Removed fp _ _)       | ".lock" `isSuffixOf` fp = True@@ -193,44 +202,6 @@     dropEnd :: Int -> [a] -> [a]     dropEnd index xs = take (length xs - index) xs --- | Find a secret key in the 'Config' or environment variable-findSigningKey ::-  Env ->-  -- | Cache name-  Text ->-  -- | Secret key or exception-  IO SigningKey-findSigningKey env name = do-  maybeEnvSK <- lookupEnv "CACHIX_SIGNING_KEY"-  -- we reverse list of caches to prioritize keys added as last-  let matches c = filter (\bc -> Config.name bc == name) $ reverse $ binaryCaches c-      maybeBCSK = case config env of-        Nothing -> Nothing-        Just c -> Config.secretKey <$> head (matches c)-  signingKey <- case maybeBCSK <|> toS <$> maybeEnvSK of-    Just key -> return key-    Nothing -> throwIO $ NoSigningKey msg-  escalateAs FatalError $ parseSigningKeyLenient signingKey-  where-    msg :: Text-    msg =-      [iTrim|-Signing key not found. --It is generated by `cachix generate-keypair <name>` and stored in ~/config/cachix/cachix.dhall--There are a few options why this happened:--a) You haven't generated signing key yet for your cache --b) You have the key but you're pushing from a different machine. -   You can set it via $CACHIX_SIGNING_KEY environment variable.--c) You've lost the signing key. In that case it's best to delete the cache and start again.-   Note that everyone that configured the binary cache will have to do it again to set the new-   public key.-      |]- retryText :: RetryStatus -> Text retryText retrystatus =   if rsIterNumber retrystatus == 0@@ -244,7 +215,7 @@       on401 =         if isJust (config env)           then throwM $ accessDeniedBinaryCache name-          else throwM notAuthenticatedBinaryCache,+          else throwM $ notAuthenticatedBinaryCache name,       onError = throwM,       onAttempt = \retrystatus size ->         -- we append newline instead of putStrLn due to https://github.com/haskell/text/issues/242@@ -253,20 +224,3 @@       withXzipCompressor = defaultWithXzipCompressorWithLevel (compressionLevel opts),       Cachix.Client.Push.omitDeriver = Cachix.Client.OptionsParser.omitDeriver opts     }--pushStorePath :: Env -> PushOptions -> Text -> Text -> IO ()-pushStorePath env opts name storePath = do-  sk <- findSigningKey env name-  -- use secret key from config or env-  -- TODO: this shouldn't happen for each store path-  store <- wait (storeAsync env)-  pushSingleStorePath-    (clientenv env)-    store-    PushCache-      { pushCacheToken = envToToken env,-        pushCacheName = name,-        pushCacheSigningKey = sk-      }-    (pushStrategy env opts name storePath)-    storePath
src/Cachix/Client/Config.hs view
@@ -1,7 +1,13 @@ {-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE QuasiQuotes #-}  module Cachix.Client.Config-  ( Config (..),+  ( Config (binaryCaches),+    getAuthTokenRequired,+    getAuthTokenOptional,+    getAuthTokenMaybe,+    setAuthToken,+    noAuthTokenError,     BinaryCacheConfig (..),     readConfig,     writeConfig,@@ -12,6 +18,8 @@ where  import Cachix.Client.Config.Orphans ()+import Cachix.Client.Exception (CachixException (..))+import Data.String.Here import Dhall hiding (Text) import Dhall.Pretty (prettyExpr) import Protolude@@ -22,6 +30,7 @@     doesFileExist,     getXdgDirectory,   )+import System.Environment (lookupEnv) import System.FilePath.Posix (takeDirectory) import System.Posix.Files   ( ownerReadMode,@@ -77,3 +86,42 @@ assureFilePermissions :: FilePath -> IO () assureFilePermissions fp =   setFileMode fp $ unionFileModes ownerReadMode ownerWriteMode++getAuthTokenRequired :: Maybe Config -> IO Token+getAuthTokenRequired maybeConfig = do+  authTokenMaybe <- getAuthTokenMaybe maybeConfig+  case authTokenMaybe of+    Just authtoken -> return authtoken+    Nothing -> throwIO $ NoConfig $ toS noAuthTokenError++-- TODO: https://github.com/haskell-servant/servant-auth/issues/173+getAuthTokenOptional :: Maybe Config -> IO Token+getAuthTokenOptional maybeConfig = do+  authTokenMaybe <- getAuthTokenMaybe maybeConfig+  return $ Protolude.maybe (Token "") identity authTokenMaybe++-- get auth token from env variable or fallback to config+getAuthTokenMaybe :: Maybe Config -> IO (Maybe Token)+getAuthTokenMaybe maybeConfig = do+  maybeAuthToken <- lookupEnv "CACHIX_AUTH_TOKEN"+  case (maybeAuthToken, maybeConfig) of+    (Just token, _) -> return $ Just $ Token $ toS token+    (Nothing, Just cfg) -> return $ Just $ authToken cfg+    (_, _) -> return Nothing++noAuthTokenError :: Text+noAuthTokenError =+  [iTrim|+Start by visiting https://app.cachix.org and either:++a) Via environment variable: ++$ export CACHIX_AUTH_TOKEN=<token...>++b) Running:++$ cachix authtoken <token...>+  |]++setAuthToken :: Config -> Token -> Config+setAuthToken cfg token = cfg {authToken = token}
src/Cachix/Client/InstallationMode.hs view
@@ -13,17 +13,20 @@   ) where -import qualified Cachix.Api as Api import Cachix.Client.Config (Config)+import qualified Cachix.Client.Config as Config import Cachix.Client.Exception (CachixException (..)) import qualified Cachix.Client.NetRc as NetRc import qualified Cachix.Client.NixConf as NixConf+import qualified Cachix.Types.BinaryCache as BinaryCache+import qualified Data.Maybe import Data.String.Here import qualified Data.Text as T import Protolude import System.Directory (Permissions, createDirectoryIfMissing, getPermissions, writable) import System.Environment (lookupEnv) import System.FilePath ((</>), replaceFileName)+import System.Process (readProcessWithExitCode) import Prelude (String)  data NixEnv@@ -44,7 +47,8 @@ data UseOptions   = UseOptions       { useMode :: Maybe InstallationMode,-        useNixOSFolder :: FilePath+        useNixOSFolder :: FilePath,+        useOutputDirectory :: Maybe FilePath       }   deriving (Show) @@ -58,12 +62,15 @@ toString :: InstallationMode -> String toString (Install NixConf.Global) = "root-nixconf" toString (Install NixConf.Local) = "user-nixconf"+toString (Install (NixConf.Custom _)) = "custom-nixconf" toString WriteNixOS = "nixos" toString UntrustedRequiresSudo = "untrusted-requires-sudo" toString UntrustedNixOS = "untrusted-nixos" -getInstallationMode :: NixEnv -> InstallationMode-getInstallationMode nixenv+getInstallationMode :: NixEnv -> UseOptions -> InstallationMode+getInstallationMode nixenv useOptions+  | (isRoot nixenv || isTrusted nixenv) && isJust (useOutputDirectory useOptions) = Install (NixConf.Custom $ Data.Maybe.fromJust $ useOutputDirectory useOptions)+  | isJust (useMode useOptions) = Data.Maybe.fromJust $ useMode useOptions   | isNixOS nixenv && isRoot nixenv = WriteNixOS   | not (isNixOS nixenv) && isRoot nixenv = Install NixConf.Global   | isTrusted nixenv = Install NixConf.Local@@ -71,7 +78,7 @@   | otherwise = UntrustedRequiresSudo  -- | Add a Binary cache to nix.conf, print nixos config or fail-addBinaryCache :: Maybe Config -> Api.BinaryCache -> UseOptions -> InstallationMode -> IO ()+addBinaryCache :: Maybe Config -> BinaryCache.BinaryCache -> UseOptions -> InstallationMode -> IO () addBinaryCache _ _ _ UntrustedNixOS = do   user <- getUser   throwIO $@@ -114,8 +121,11 @@       NixConf.Local -> do         lnc <- NixConf.read NixConf.Local         return ([gnc, lnc], lnc)+      NixConf.Custom _ -> do+        lnc <- NixConf.read ncl+        return ([lnc], lnc)   let nixconf = fromMaybe (NixConf.NixConf []) output-  netrcLocMaybe <- forM (guard $ not (Api.isPublic bc)) $ const $ addPrivateBinaryCacheNetRC maybeConfig bc ncl+  netrcLocMaybe <- forM (guard $ not (BinaryCache.isPublic bc)) $ const $ addPrivateBinaryCacheNetRC maybeConfig bc ncl   let addNetRCLine :: NixConf.NixConf -> NixConf.NixConf       addNetRCLine = fromMaybe identity $ do         netrcLoc <- netrcLocMaybe :: Maybe FilePath@@ -125,9 +135,9 @@         pure (NixConf.setNetRC $ toS netrcLoc)   NixConf.write ncl $ addNetRCLine $ NixConf.add bc (catMaybes input) nixconf   filename <- NixConf.getFilename ncl-  putStrLn $ "Configured " <> Api.uri bc <> " binary cache in " <> toS filename+  putStrLn $ "Configured " <> BinaryCache.uri bc <> " binary cache in " <> toS filename -nixosBinaryCache :: Maybe Config -> Api.BinaryCache -> UseOptions -> IO ()+nixosBinaryCache :: Maybe Config -> BinaryCache.BinaryCache -> UseOptions -> IO () nixosBinaryCache maybeConfig bc UseOptions {useNixOSFolder = baseDirectory} = do   _ <- try $ createDirectoryIfMissing True $ toS toplevel :: IO (Either SomeException ())   eitherPermissions <- try $ getPermissions (toS toplevel) :: IO (Either SomeException Permissions)@@ -140,7 +150,7 @@     installFiles = do       writeFile (toS glueModuleFile) glueModule       writeFile (toS cacheModuleFile) cacheModule-      unless (Api.isPublic bc) $ void $ addPrivateBinaryCacheNetRC maybeConfig bc NixConf.Global+      unless (BinaryCache.isPublic bc) $ void $ addPrivateBinaryCacheNetRC maybeConfig bc NixConf.Global       putText instructions     configurationNix :: Text     configurationNix = toS $ toS baseDirectory </> "configuration.nix"@@ -151,7 +161,7 @@     glueModuleFile :: Text     glueModuleFile = toplevel <> ".nix"     cacheModuleFile :: Text-    cacheModuleFile = toplevel <> "/" <> toS (Api.name bc) <> ".nix"+    cacheModuleFile = toplevel <> "/" <> toS (BinaryCache.name bc) <> ".nix"     noEtcPermissionInstructions :: Text -> Text     noEtcPermissionInstructions dir =       [iTrim|@@ -163,7 +173,7 @@     instructions =       [iTrim| Cachix configuration written to ${glueModuleFile}.-Binary cache ${Api.name bc} configuration written to ${cacheModuleFile}.+Binary cache ${BinaryCache.name bc} configuration written to ${cacheModuleFile}.  To start using cachix add the following to your ${configurationNix}: @@ -195,21 +205,21 @@ {   nix = {     binaryCaches = [-      "${Api.uri bc}"+      "${BinaryCache.uri bc}"     ];     binaryCachePublicKeys = [-      ${T.intercalate " " (map (\s -> "\"" <> s <> "\"") (Api.publicSigningKeys bc))}+      ${T.intercalate " " (map (\s -> "\"" <> s <> "\"") (BinaryCache.publicSigningKeys bc))}     ];   }; }     |]  -- TODO: allow overriding netrc location-addPrivateBinaryCacheNetRC :: Maybe Config -> Api.BinaryCache -> NixConf.NixConfLoc -> IO FilePath+addPrivateBinaryCacheNetRC :: Maybe Config -> BinaryCache.BinaryCache -> NixConf.NixConfLoc -> IO FilePath addPrivateBinaryCacheNetRC maybeConfig bc nixconf = do   filename <- (`replaceFileName` "netrc") <$> NixConf.getFilename nixconf   case maybeConfig of-    Nothing -> panic "Run $ cachix authtoken <token>"+    Nothing -> throwIO $ NoConfig Config.noAuthTokenError     Just config -> do       let netrcfile = fromMaybe filename Nothing -- TODO: get netrc from nixconf       NetRc.add config [bc] netrcfile@@ -221,14 +231,19 @@   user <- getUser   -- to detect single user installations   permissions <- getPermissions "/nix/store"-  let isTrustedU = writable permissions || user `elem` users-  when (not (null groups) && not isTrustedU) $ do-    -- TODO: support Nix group syntax-    putText "Warn: cachix doesn't yet support checking if user is trusted via groups, so it will recommend sudo"-    putStrLn $ "Warn: groups found " <> T.intercalate "," groups-  return isTrustedU+  isInAGroup <- userInAnyGroup user+  return $ writable permissions || user `elem` users || isInAGroup   where-    groups = filter (\u -> T.head u == '@') users+    groups :: [Text]+    groups = map T.tail $ filter (\u -> T.head u == '@') users+    userInAnyGroup :: Text -> IO Bool+    userInAnyGroup user = do+      isIn <- for groups $ checkUserInGroup user+      return $ any identity isIn+    checkUserInGroup :: Text -> Text -> IO Bool+    checkUserInGroup user groupName = do+      (_exitcode, out, _err) <- readProcessWithExitCode "id" ["-Gn", toS user] mempty+      return $ groupName `T.isInfixOf` toS out  getUser :: IO Text getUser = do
src/Cachix/Client/NetRc.hs view
@@ -4,16 +4,17 @@   ) where -import qualified Cachix.Api as Api-import Cachix.Api.Error (escalateAs)-import Cachix.Client.Config (Config, authToken)+import Cachix.API.Error (escalateAs)+import Cachix.Client.Config (Config)+import qualified Cachix.Client.Config as Config import Cachix.Client.Exception (CachixException (NetRcParseError))+import qualified Cachix.Types.BinaryCache as BinaryCache import qualified Data.ByteString as BS import Data.List (nubBy) import qualified Data.Text as T import Network.NetRc import Protolude-import Servant.Auth.Client (getToken)+import Servant.Auth.Client (Token, getToken) import System.Directory (createDirectoryIfMissing, doesFileExist) import System.FilePath (takeDirectory) @@ -22,34 +23,35 @@ --   If file under filename doesn't exist it's created. add ::   Config ->-  [Api.BinaryCache] ->+  [BinaryCache.BinaryCache] ->   FilePath ->   IO () add config binarycaches filename = do   doesExist <- doesFileExist filename+  cachixAuthToken <- Config.getAuthTokenRequired (Just config)   netrc <-     if doesExist       then BS.readFile filename >>= parse       else return $ NetRc [] []   createDirectoryIfMissing True (takeDirectory filename)-  BS.writeFile filename $ netRcToByteString $ uniqueAppend netrc+  BS.writeFile filename $ netRcToByteString $ uniqueAppend cachixAuthToken netrc   where     parse :: ByteString -> IO NetRc     parse contents = escalateAs (NetRcParseError . show) $ parseNetRc filename contents     -- O(n^2) but who cares?-    uniqueAppend :: NetRc -> NetRc-    uniqueAppend (NetRc hosts macdefs) =+    uniqueAppend :: Token -> NetRc -> NetRc+    uniqueAppend cachixAuthToken (NetRc hosts macdefs) =       let f :: NetRcHost -> NetRcHost -> Bool           f x y = nrhName x == nrhName y-       in NetRc (nubBy f (new ++ hosts)) macdefs-    new :: [NetRcHost]-    new = map mkHost $ filter (not . Api.isPublic) binarycaches-    mkHost :: Api.BinaryCache -> NetRcHost-    mkHost bc =+       in NetRc (nubBy f (new cachixAuthToken ++ hosts)) macdefs+    new :: Token -> [NetRcHost]+    new cachixAuthToken = map (mkHost cachixAuthToken) $ filter (not . BinaryCache.isPublic) binarycaches+    mkHost :: Token -> BinaryCache.BinaryCache -> NetRcHost+    mkHost cachixAuthToken bc =       NetRcHost-        { nrhName = toS $ stripPrefix "http://" $ stripPrefix "https://" (Api.uri bc),+        { nrhName = toS $ stripPrefix "http://" $ stripPrefix "https://" (BinaryCache.uri bc),           nrhLogin = "",-          nrhPassword = getToken (authToken config),+          nrhPassword = getToken cachixAuthToken,           nrhAccount = "",           nrhMacros = []         }
src/Cachix/Client/NixConf.hs view
@@ -32,7 +32,7 @@   ) where -import qualified Cachix.Api as Api+import qualified Cachix.Types.BinaryCache as BinaryCache import Data.Char (isSpace) import Data.List (nub) import qualified Data.Text as T@@ -85,14 +85,14 @@ isTrustedUsers _ = Nothing  -- | Pure version of addIO-add :: Api.BinaryCache -> [NixConf] -> NixConf -> NixConf+add :: BinaryCache.BinaryCache -> [NixConf] -> NixConf -> NixConf add bc toRead toWrite =   writeLines isPublicKey (TrustedPublicKeys $ nub publicKeys) $     writeLines isSubstituter (Substituters $ nub substituters) toWrite   where     -- Note: some defaults are always appended since overriding some setttings in nix.conf overrides defaults otherwise-    substituters = (defaultPublicURI : readLines toRead isSubstituter) <> [Api.uri bc]-    publicKeys = (defaultSigningKey : readLines toRead isPublicKey) <> Api.publicSigningKeys bc+    substituters = (defaultPublicURI : readLines toRead isSubstituter) <> [BinaryCache.uri bc]+    publicKeys = (defaultSigningKey : readLines toRead isPublicKey) <> BinaryCache.publicSigningKeys bc  defaultPublicURI :: Text defaultPublicURI = "https://cache.nixos.org"@@ -141,7 +141,7 @@     noNetRc (NetRcFile _) = False     noNetRc _ = True -data NixConfLoc = Global | Local+data NixConfLoc = Global | Local | Custom FilePath   deriving (Show, Eq)  getFilename :: NixConfLoc -> IO FilePath@@ -150,6 +150,7 @@     case ncl of       Global -> return "/etc/nix"       Local -> getXdgDirectory XdgConfig "nix"+      Custom filepath -> return filepath   return $ dir <> "/nix.conf"  -- nix.conf Parser
src/Cachix/Client/OptionsParser.hs view
@@ -85,14 +85,14 @@ parserCachixCommand :: Parser CachixCommand parserCachixCommand =   subparser $-    command "authtoken" (infoH authtoken (progDesc "Configure token for authentication to cachix.org"))+    command "authtoken" (infoH authtoken (progDesc "Configure authentication token for communication to cachix.org API"))       <> command "create" (infoH create (progDesc "DEPRECATED: Go to https://cachix.org instead"))       <> command "generate-keypair" (infoH generateKeypair (progDesc "Generate keypair for a binary cache"))-      <> command "push" (infoH push (progDesc "Upload Nix store paths to the binary cache"))-      <> command "use" (infoH use (progDesc "Configure nix.conf to enable binary cache during builds"))+      <> command "push" (infoH push (progDesc "Upload Nix store paths to a binary cache"))+      <> command "use" (infoH use (progDesc "Configure a binary cache by writing nix.conf and netrc files."))   where-    nameArg = strArgument (metavar "NAME")-    authtoken = AuthToken <$> strArgument (metavar "TOKEN")+    nameArg = strArgument (metavar "CACHE-NAME")+    authtoken = AuthToken <$> strArgument (metavar "AUTHTOKEN")     create = Create <$> nameArg     generateKeypair = GenerateKeypair <$> nameArg     validatedLevel l =@@ -146,9 +146,16 @@                 <*> strOption                   ( long "nixos-folder"                       <> short 'd'-                      <> help "Base directory for NixOS configuration"+                      <> help "Base directory for NixOS configuration generation"                       <> value "/etc/nixos/"                       <> showDefault+                  )+                <*> optional+                  ( strOption+                      ( long "output-directory"+                          <> short 'O'+                          <> help "Output directory where nix.conf and netrc will be updated."+                      )                   )             ) 
src/Cachix/Client/Push.hs view
@@ -1,15 +1,19 @@ {-# LANGUAGE DataKinds #-}+{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE Rank2Types #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeOperators #-} +{- This is a standalone module so it shouldn't depend on any CLI state like Env -} module Cachix.Client.Push   ( -- * Pushing a single path     pushSingleStorePath,     PushCache (..),+    PushSecret (..),     PushStrategy (..),     defaultWithXzipCompressor,     defaultWithXzipCompressorWithLevel,+    findPushSecret,      -- * Pushing a closure of store paths     pushClosure,@@ -17,15 +21,18 @@   ) where -import qualified Cachix.Api as Api-import Cachix.Api.Error-import Cachix.Api.Signing (fingerprint, passthroughHashSink, passthroughHashSinkB16, passthroughSizeSink)+import qualified Cachix.API as API+import Cachix.API.Error+import Cachix.API.Signing (fingerprint, passthroughHashSink, passthroughHashSinkB16, passthroughSizeSink)+import qualified Cachix.Client.Config as Config import Cachix.Client.Exception (CachixException (..)) import Cachix.Client.Secrets import Cachix.Client.Servant import Cachix.Client.Store (Store) import qualified Cachix.Client.Store as Store+import qualified Cachix.Types.ByteStringStreaming import qualified Cachix.Types.NarInfoCreate as Api+import qualified Cachix.Types.NarInfoHash as NarInfoHash import Control.Concurrent.Async (mapConcurrently) import qualified Control.Concurrent.QSem as QSem import Control.Exception.Safe (MonadMask, throwM)@@ -33,26 +40,32 @@ import Control.Retry (RetryPolicy, RetryStatus, exponentialBackoff, limitRetries, recoverAll) import Crypto.Sign.Ed25519 import qualified Data.ByteString.Base64 as B64+import Data.Coerce (coerce) import Data.Conduit import Data.Conduit.Lzma (compress)-import Data.Conduit.Process+import Data.Conduit.Process hiding (env) import Data.IORef import qualified Data.Set as Set+import Data.String.Here import qualified Data.Text as T import Network.HTTP.Types (status401, status404) import Protolude import Servant.API import Servant.Auth () import Servant.Auth.Client-import Servant.Client.Streaming hiding (ClientError)+import Servant.Client.Streaming import Servant.Conduit ()+import System.Environment (lookupEnv) import qualified System.Nix.Base32 +data PushSecret+  = PushToken Token+  | PushSigningKey SigningKey+ data PushCache   = PushCache       { pushCacheName :: Text,-        pushCacheSigningKey :: SigningKey,-        pushCacheToken :: Token+        pushCacheSecret :: PushSecret       }  data PushStrategy m r@@ -90,13 +103,13 @@   let storeHash = fst $ splitStorePath $ toS storePath       name = pushCacheName cache   -- Check if narinfo already exists-  -- TODO: query also cache.nixos.org? server-side?   res <-     liftIO $ (`runClientM` clientEnv) $-      Api.narinfoHead-        (cachixBCClient name)-        (pushCacheToken cache)-        (Api.NarInfoC storeHash)+      API.narinfoHead+        cachixClient+        (getCacheAuthToken cache)+        name+        (NarInfoHash.NarInfoHash storeHash)   case res of     Right NoContent -> onAlreadyPresent cb -- we're done as store path is already in the cache     Left err@@ -104,6 +117,11 @@       | isErr err status401 -> on401 cb       | otherwise -> onError cb err +getCacheAuthToken :: PushCache -> Token+getCacheAuthToken cache = case pushCacheSecret cache of+  PushToken token -> token+  PushSigningKey _ -> Token ""+ uploadStorePath ::   (MonadMask m, MonadIO m) =>   -- | cachix base url, connection manager, see 'Cachix.Client.URI.defaultCachixBaseUrl', 'Servant.Client.mkClientEnv'@@ -153,7 +171,7 @@     (_ :: NoContent) <-       liftIO         $ (`withClientM` newClientEnv)-          (Api.createNar (cachixBCStreamingClient name) stream')+          (API.createNar cachixClient (getCacheAuthToken cache) name (mapOutput coerce stream'))         $ escalate           >=> \NoContent -> do             exitcode <- waitForStreamingProcess cph@@ -173,7 +191,9 @@       referencesPathSet <- Store.validPathInfoReferences pathinfo       references <- sort <$> Store.traversePathSet (pure . toS) referencesPathSet       let fp = fingerprint storePath narHash narSize references-          sig = dsign (signingSecretKey $ pushCacheSigningKey cache) fp+          (sig, authToken) = case pushCacheSecret cache of+            PushToken token -> (Nothing, token)+            PushSigningKey signKey -> (Just $ toS $ B64.encode $ unSignature $ dsign (signingSecretKey signKey) fp, Token "")           nic =             Api.NarInfoCreate               { Api.cStoreHash = storeHash,@@ -187,14 +207,16 @@                   if deriver == Store.unknownDeriver                     then deriver                     else T.drop 11 deriver,-                Api.cSig = toS $ B64.encode $ unSignature sig+                Api.cSig = sig               }       escalate $ Api.isNarInfoCreateValid nic       -- Upload narinfo with signature       escalate <=< (`runClientM` clientEnv) $-        Api.createNarinfo-          (cachixBCClient name)-          (Api.NarInfoC storeHash)+        API.createNarinfo+          cachixClient+          authToken+          name+          (NarInfoHash.NarInfoHash storeHash)           nic     onDone cb @@ -235,21 +257,55 @@       closure <- Store.computeFSClosure store Store.defaultClosureParams inputs       Store.traversePathSet (pure . toSL) closure   -- Check what store paths are missing-  -- TODO: query also cache.nixos.org? server-side?   missingHashesList <-     retryAll $ \_ ->       escalate         =<< liftIO           ( (`runClientM` clientEnv) $-              Api.narinfoBulk-                (cachixBCClient (pushCacheName pushCache))-                (pushCacheToken pushCache)+              API.narinfoBulk+                cachixClient+                (getCacheAuthToken pushCache)+                (pushCacheName pushCache)                 (fst . splitStorePath <$> paths)           )   let missingHashes = Set.fromList missingHashesList       missingPaths = filter (\path -> Set.member (fst (splitStorePath path)) missingHashes) paths-  -- TODO: make pool size configurable, on beefier machines this could be doubled   traversal (\path -> retryAll $ \retrystatus -> uploadStorePath clientEnv store pushCache (pushStrategy path) path retrystatus) missingPaths++-- TODO: move to a separate module specific to cli++-- | Find auth token or signing key in the 'Config' or environment variable+findPushSecret ::+  Maybe Config.Config ->+  -- | Cache name+  Text ->+  -- | Secret key or exception+  IO PushSecret+findPushSecret config name = do+  maybeEnvSK <- lookupEnv "CACHIX_SIGNING_KEY"+  -- we reverse list of caches to prioritize keys added as last+  let matches c = filter (\bc -> Config.name bc == name) $ reverse $ Config.binaryCaches c+      maybeBCSK = case config of+        Nothing -> Nothing+        Just c -> Config.secretKey <$> head (matches c)+  case maybeBCSK <|> toS <$> maybeEnvSK of+    Just signingKey -> escalateAs FatalError $ PushSigningKey <$> parseSigningKeyLenient signingKey+    Nothing -> do+      authtoken <- Config.getAuthTokenMaybe config+      case authtoken of+        Just at -> return $ PushToken at+        Nothing -> throwIO $ NoSigningKey msg+  where+    msg :: Text+    msg =+      [iTrim|+Neither auth token nor signing key are present.++They are looked up via $CACHIX_AUTH_TOKEN and $CACHIX_SIGNING_KEY,+and if missing also looked up from ~/.config/cachix/cachix.dhall++Read https://mycache.cachix.org for instructions how to push to your binary cache.+    |]  mapConcurrentlyBounded :: Traversable t => Int -> (a -> IO b) -> t a -> IO (t b) mapConcurrentlyBounded bound action items = do
src/Cachix/Client/Servant.hs view
@@ -1,63 +1,32 @@-{-# LANGUAGE CPP #-} {-# LANGUAGE DataKinds #-}-{-# LANGUAGE TypeOperators #-} {-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-}-{-# OPTIONS_GHC -O0 #-} -- TODO https://github.com/haskell-servant/servant/issues/986+{-# OPTIONS_GHC -O0 #-} -module Cachix.Client.Servant-  ( isErr-  , cachixClient-  , cachixBCClient-  , cachixBCStreamingClient-  , runAuthenticatedClient-  , Cachix.Client.Servant.ClientError-  ) where+-- TODO https://github.com/haskell-servant/servant/issues/986 -import           Protolude+module Cachix.Client.Servant+  ( isErr,+    cachixClient,+  )+where -import qualified Cachix.Api as Api-import           Cachix.Api.Error-import qualified Cachix.Client.Config as Config-import qualified Cachix.Client.Env as Env-import qualified Cachix.Client.Exception as Exception-import           Network.HTTP.Types (Status)-import           Servant.API.Generic-import           Servant.Auth             ()-import           Servant.Auth.Client      (Token)+import qualified Cachix.API+import Network.HTTP.Types (Status)+import Protolude+import Servant.API.Generic+import Servant.Auth.Client () import qualified Servant.Client-import           Servant.Client.Generic   (AsClientT)-import           Servant.Client.Streaming hiding (ClientError)-import           Servant.Conduit          ()+import Servant.Client.Generic (AsClientT)+import Servant.Client.Streaming+import Servant.Conduit () -type ClientError =-#if !MIN_VERSION_servant_client(0,16,0)-  Servant.Client.ServantError-#else-  Servant.Client.ClientError-#endif  isErr :: ClientError -> Status -> Bool-#if MIN_VERSION_servant_client(0,16,0) isErr (Servant.Client.FailureResponse _ resp) status-#else-isErr (Servant.Client.FailureResponse resp) status-#endif   | Servant.Client.responseStatusCode resp == status = True isErr _ _ = False -cachixClient :: Api.CachixAPI (AsClientT ClientM)-cachixClient = fromServant $ client Api.servantApi--cachixBCClient :: Text -> Api.BinaryCacheAPI (AsClientT ClientM)-cachixBCClient name = fromServant $ Api.cache cachixClient name--cachixBCStreamingClient :: Text -> Api.BinaryCacheStreamingAPI (AsClientT ClientM)-cachixBCStreamingClient name = fromServant $ client (Proxy :: Proxy Api.BinaryCachStreamingServantAPI) name--runAuthenticatedClient :: NFData a => Env.Env -> (Token -> ClientM a) -> IO a-runAuthenticatedClient env m = do-  config <- escalate $ maybeToEither (Exception.NoConfig-     "Start with visiting https://cachix.org and copying the token to $ cachix authtoken <token>") (Env.config env)-  escalate <=< (`runClientM` Env.clientenv env) $-    m (Config.authToken config)+cachixClient :: Cachix.API.BinaryCacheAPI (AsClientT ClientM)+cachixClient = fromServant $ client Cachix.API.api
test/InstallationModeSpec.hs view
@@ -5,6 +5,14 @@ import Protolude import Test.Hspec +defautUseOptions :: UseOptions+defautUseOptions =+  UseOptions+    { useMode = Nothing,+      useOutputDirectory = Nothing,+      useNixOSFolder = "/etc/nixos"+    }+ spec :: Spec spec = describe "getInstallationMode" $ do   it "NixOS with root prints configuration" $@@ -14,7 +22,7 @@               isRoot = True,               isNixOS = True             }-     in getInstallationMode nixenv `shouldBe` WriteNixOS+     in getInstallationMode nixenv defautUseOptions `shouldBe` WriteNixOS   it "NixOS without trust prints steps to follow" $     let nixenv =           NixEnv@@ -22,7 +30,7 @@               isRoot = False,               isNixOS = True             }-     in getInstallationMode nixenv `shouldBe` UntrustedNixOS+     in getInstallationMode nixenv defautUseOptions `shouldBe` UntrustedNixOS   it "NixOS without trust prints steps to follow" $     let nixenv =           NixEnv@@ -30,7 +38,7 @@               isRoot = False,               isNixOS = True             }-     in getInstallationMode nixenv `shouldBe` UntrustedNixOS+     in getInstallationMode nixenv defautUseOptions `shouldBe` UntrustedNixOS   it "NixOS non-root trusted results into local install" $     let nixenv =           NixEnv@@ -38,7 +46,7 @@               isRoot = False,               isNixOS = True             }-     in getInstallationMode nixenv `shouldBe` Install NixConf.Local+     in getInstallationMode nixenv defautUseOptions `shouldBe` Install NixConf.Local   it "non-NixOS root results into global install" $     let nixenv =           NixEnv@@ -46,7 +54,7 @@               isRoot = True,               isNixOS = False             }-     in getInstallationMode nixenv `shouldBe` Install NixConf.Global+     in getInstallationMode nixenv defautUseOptions `shouldBe` Install NixConf.Global   it "non-NixOS with Nix 1.X root results into global install" $     let nixenv =           NixEnv@@ -54,7 +62,7 @@               isRoot = True,               isNixOS = False -- any             }-     in getInstallationMode nixenv `shouldBe` Install NixConf.Global+     in getInstallationMode nixenv defautUseOptions `shouldBe` Install NixConf.Global   it "non-NixOS non-root trusted results into local install" $     let nixenv =           NixEnv@@ -62,7 +70,7 @@               isRoot = False,               isNixOS = False             }-     in getInstallationMode nixenv `shouldBe` Install NixConf.Local+     in getInstallationMode nixenv defautUseOptions `shouldBe` Install NixConf.Local   it "non-NixOS non-root non-trusted results into required sudo" $     let nixenv =           NixEnv@@ -70,4 +78,4 @@               isRoot = False,               isNixOS = False             }-     in getInstallationMode nixenv `shouldBe` UntrustedRequiresSudo+     in getInstallationMode nixenv defautUseOptions `shouldBe` UntrustedRequiresSudo
test/NetRcSpec.hs view
@@ -1,8 +1,9 @@ module NetRcSpec where -import Cachix.Api (BinaryCache (..))-import Cachix.Client.Config (Config (..))+import Cachix.Client.Config (Config, mkConfig) import qualified Cachix.Client.NetRc as NetRc+import Cachix.Types.BinaryCache (BinaryCache (..))+import Cachix.Types.Permission (Permission (..)) import Protolude import System.Directory (copyFile) import System.IO.Temp (withSystemTempFile)@@ -15,7 +16,8 @@       uri = "https://name.cachix.org",       publicSigningKeys = ["pub"],       isPublic = False,-      githubUsername = "foobar"+      githubUsername = "foobar",+      permission = Read     }  bc2 :: BinaryCache@@ -25,15 +27,12 @@       uri = "https://name2.cachix.org",       publicSigningKeys = ["pub2"],       isPublic = False,-      githubUsername = "foobar2"+      githubUsername = "foobar2",+      permission = Read     }  config :: Config-config =-  Config-    { authToken = "token123",-      binaryCaches = []-    }+config = mkConfig "token123"  -- TODO: poor man's golden tests, use https://github.com/stackbuilders/hspec-golden test :: [BinaryCache] -> Text -> Expectation
test/NixConfSpec.hs view
@@ -2,8 +2,8 @@  module NixConfSpec where -import Cachix.Api (BinaryCache (..)) import Cachix.Client.NixConf as NixConf+import Cachix.Types.BinaryCache (BinaryCache (..)) import Data.String.Here import Protolude import Test.Hspec