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 +15/−1
- cachix.cabal +4/−4
- src/Cachix/Client/Commands.hs +45/−91
- src/Cachix/Client/Config.hs +49/−1
- src/Cachix/Client/InstallationMode.hs +37/−22
- src/Cachix/Client/NetRc.hs +17/−15
- src/Cachix/Client/NixConf.hs +6/−5
- src/Cachix/Client/OptionsParser.hs +13/−6
- src/Cachix/Client/Push.hs +79/−23
- src/Cachix/Client/Servant.hs +18/−49
- test/InstallationModeSpec.hs +16/−8
- test/NetRcSpec.hs +8/−9
- test/NixConfSpec.hs +1/−1
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