hercules-ci-agent 0.7.5 → 0.8.0
raw patch · 63 files changed
+2042/−2054 lines, 63 filesdep +hercules-ci-cnix-exprdep +hercules-ci-cnix-storedep +process-extrasdep ~basedep ~cachixdep ~hercules-ci-api-agent
Dependencies added: hercules-ci-cnix-expr, hercules-ci-cnix-store, process-extras
Dependency ranges changed: base, cachix, hercules-ci-api-agent, hercules-ci-api-core, protolude
Files
- CHANGELOG.md +74/−1
- cbits/hercules-aliases.h +2/−0
- hercules-ci-agent-worker/Hercules/Agent/Worker.hs +262/−186
- hercules-ci-agent-worker/Hercules/Agent/Worker/Build.hs +21/−20
- hercules-ci-agent-worker/Hercules/Agent/Worker/Build/Logger.hs +77/−70
- hercules-ci-agent-worker/Hercules/Agent/Worker/Build/Logger/Context.hs +35/−0
- hercules-ci-agent-worker/Hercules/Agent/Worker/Build/Prefetched.hs +21/−23
- hercules-ci-agent-worker/Hercules/Agent/Worker/Effect.hs +49/−0
- hercules-ci-agent-worker/Hercules/Agent/Worker/HerculesStore.hs +102/−0
- hercules-ci-agent-worker/Hercules/Agent/Worker/HerculesStore/Context.hs +35/−0
- hercules-ci-agent.cabal +43/−64
- hercules-ci-agent/Data/Functor/Partitioner.hs +26/−25
- hercules-ci-agent/Hercules/Agent.hs +50/−46
- hercules-ci-agent/Hercules/Agent/AgentSocket.hs +2/−2
- hercules-ci-agent/Hercules/Agent/Build.hs +21/−15
- hercules-ci-agent/Hercules/Agent/Cache.hs +5/−9
- hercules-ci-agent/Hercules/Agent/Cachix.hs +28/−26
- hercules-ci-agent/Hercules/Agent/Cachix/Env.hs +13/−9
- hercules-ci-agent/Hercules/Agent/Cachix/Init.hs +31/−19
- hercules-ci-agent/Hercules/Agent/Client.hs +28/−9
- hercules-ci-agent/Hercules/Agent/Config.hs +54/−33
- hercules-ci-agent/Hercules/Agent/Config/BinaryCaches.hs +18/−17
- hercules-ci-agent/Hercules/Agent/Effect.hs +91/−0
- hercules-ci-agent/Hercules/Agent/Env.hs +26/−28
- hercules-ci-agent/Hercules/Agent/EnvironmentInfo.hs +73/−67
- hercules-ci-agent/Hercules/Agent/Evaluate.hs +171/−158
- hercules-ci-agent/Hercules/Agent/Evaluate/TraversalQueue.hs +10/−10
- hercules-ci-agent/Hercules/Agent/Files.hs +41/−0
- hercules-ci-agent/Hercules/Agent/Init.hs +20/−19
- hercules-ci-agent/Hercules/Agent/Log.hs +4/−4
- hercules-ci-agent/Hercules/Agent/Nix.hs +19/−0
- hercules-ci-agent/Hercules/Agent/Nix/Env.hs +3/−4
- hercules-ci-agent/Hercules/Agent/Nix/Init.hs +4/−3
- hercules-ci-agent/Hercules/Agent/Nix/RetrieveDerivationInfo.hs +22/−22
- hercules-ci-agent/Hercules/Agent/Options.hs +4/−5
- hercules-ci-agent/Hercules/Agent/ServiceInfo.hs +18/−17
- hercules-ci-agent/Hercules/Agent/Token.hs +22/−21
- hercules-ci-agent/Hercules/Agent/WorkerProcess.hs +45/−46
- src-cnix/CNix.hs +0/−263
- src-cnix/CNix/Internal/Context.hs +0/−81
- src-cnix/CNix/Internal/Raw.hs +0/−109
- src-cnix/CNix/Internal/Store.hs +0/−415
- src-cnix/CNix/Internal/Typed.hs +0/−77
- src-internal-ffi/Hercules/Agent/StoreFFI.hs +0/−20
- src/Data/Conduit/Katip/Orphans.hs +0/−2
- src/Hercules/Agent/NixFile.hs +44/−0
- src/Hercules/Agent/Producer.hs +7/−8
- src/Hercules/Agent/Sensitive.hs +22/−0
- src/Hercules/Agent/Socket.hs +40/−39
- src/Hercules/Agent/WorkerProtocol/Command.hs +2/−0
- src/Hercules/Agent/WorkerProtocol/Command/Build.hs +6/−7
- src/Hercules/Agent/WorkerProtocol/Command/BuildResult.hs +2/−2
- src/Hercules/Agent/WorkerProtocol/Command/Effect.hs +19/−0
- src/Hercules/Agent/WorkerProtocol/Command/Eval.hs +10/−11
- src/Hercules/Agent/WorkerProtocol/Event.hs +1/−0
- src/Hercules/Agent/WorkerProtocol/Event/Attribute.hs +13/−6
- src/Hercules/Agent/WorkerProtocol/Event/AttributeError.hs +6/−7
- src/Hercules/Agent/WorkerProtocol/Event/BuildResult.hs +10/−11
- src/Hercules/Agent/WorkerProtocol/LogSettings.hs +9/−15
- src/Hercules/Effect.hs +140/−0
- src/Hercules/Effect/Container.hs +138/−0
- test/Hercules/Agent/Nix/RetrieveDerivationInfoSpec.hs +1/−1
- test/TestMain.hs +2/−2
CHANGELOG.md view
@@ -5,6 +5,77 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.8.0]++### Added++ - Hercules CI Effects, a new feature for running programs that interact with+ the real world, with useful features for continous deployment.++ - Effects only run after the build completes successfully++ - Effects are defined like a derivation, not unlike a Nix shell++ - Independent processes can run concurrently as distinct effects++ - No two commits in the same repo run effects at the same time; no need to+ worry about concurrency in deployment scripts++ - Effects each run in their own sandbox with access to network, Nix store,+ remote state file API and secrets++ - Secrets are configured locally on your agents, so you don't have to trust+ a third party with your cloud credentials++ - Hercules CI Agent is now a flake. The highlights are+ - `nixosModules` overriding the NixOS-distributed module to the in-repo version+ - `agent-profile` for agent machines, or+ - `agent-service` for just the service definition+ - `packages`+ - `hercules-ci-cli` the user command line interface+ - `hercules-ci-agent` for custom installation methods, etc++ - The `hci` command (flake: `defaultApp`)+ - `hci login` to authenticate yourself+ - `hci state` to work with Effects state files+ - `hci effect` to run effects locally++ - Commit metadata as a `ci.nix` argument. Make your `ci.nix` a function:++ ```nix+ { src ? { ref = null; rev = null; }}:+ # rest of your ci.nix+ ```++ `src.ref` will have e.g. `refs/heads/master` and `rev` will have the+ git commit SHA.++ - Shell derivations will only be built for their dependencies. Add a+ `mkShell`-based expression like you would add a derivation.++ This behavior can be requested explicitly for shells and non-shell+ derivations alike by appending ` // { buildDependenciesOnly = true; }` to+ the attribute definition.++ - Attributes can now be marked to require or ignore a build failure in the+ derivation it references directly.+ ([support#34](https://github.com/hercules-ci/support/issues/34))++ - `concurrentTasks` now has a default, `"auto"` for ease of setup and to help+ avoid underutilization.++### Fixed++ - The parent directory name will match the repo name [support#40](https://github.com/hercules-ci/support/issues/40)++ - Previously, lines from Nix's configured netrc file were ignored. Now they are appended to Hercules CI's netrc lines.++### Changed++ - Cachix caches without `signingKeys` will be pushed to, as part of the recently+ introduced write token feature (Cachix-managed signing keys)++ ## [0.7.5] - 2020-11-27 ### Fixed@@ -47,7 +118,7 @@ normal NixOS expectations. Notably, it does not configure automatic garbage collection and it does not preconfigure NixOps keys deployment. - The configuration interface will remain unchanged for `0.7` but `0.8` will+ The configuration interface in the hercules-ci-agent repo will remain unchanged for `0.7` but `0.8` will match the upstream interface. ## [0.7.3] - 2020-07-18@@ -330,6 +401,8 @@ - Initial release +[0.8.0]: https://github.com/hercules-ci/hercules-ci-agent/compare/hercules-ci-agent-0.7.5...hercules-ci-agent-0.8.0+[0.7.5]: https://github.com/hercules-ci/hercules-ci-agent/compare/hercules-ci-agent-0.7.4...hercules-ci-agent-0.7.5 [0.7.4]: https://github.com/hercules-ci/hercules-ci-agent/compare/hercules-ci-agent-0.7.3...hercules-ci-agent-0.7.4 [0.7.3]: https://github.com/hercules-ci/hercules-ci-agent/compare/hercules-ci-agent-0.7.2...hercules-ci-agent-0.7.3 [0.7.2]: https://github.com/hercules-ci/hercules-ci-agent/compare/hercules-ci-agent-0.7.1...hercules-ci-agent-0.7.2
cbits/hercules-aliases.h view
@@ -3,6 +3,8 @@ #include "hercules-store.hh" #include "hercules-logger.hh" #include "derivations.hh"+#include <hercules-ci-cnix/store.hxx>+#include <hercules-ci-cnix/expr.hxx> // inline-c-cpp doesn't seem to handle namespace operator or template // syntax so we help it a bit for now. This definition can be inlined
hercules-ci-agent-worker/Hercules/Agent/Worker.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE BlockArguments #-} {-# LANGUAGE DataKinds #-}+{-# LANGUAGE LambdaCase #-} {-# LANGUAGE NumericUnderscores #-} module Hercules.Agent.Worker@@ -7,11 +8,10 @@ ) where -import CNix-import qualified CNix.Internal.Raw import Conduit import Control.Concurrent.STM import qualified Control.Exception.Lifted as EL+import Control.Monad.Except import Control.Monad.IO.Unlift import Control.Monad.Trans.Control import qualified Data.ByteString as BS@@ -25,7 +25,6 @@ import Data.IORef import qualified Data.Map as M import qualified Data.Set as S-import Data.Typeable (typeOf) import Data.UUID (UUID) import Data.Vector (Vector) import qualified Data.Vector as V@@ -34,26 +33,36 @@ import qualified Hercules.API.Logs.LogEntry as LogEntry import Hercules.API.Logs.LogMessage (LogMessage) import qualified Hercules.API.Logs.LogMessage as LogMessage+import Hercules.Agent.Sensitive import qualified Hercules.Agent.Socket as Socket-import Hercules.Agent.Worker.Build+import Hercules.Agent.Worker.Build (runBuild) import qualified Hercules.Agent.Worker.Build.Logger as Logger-import qualified Hercules.Agent.WorkerProtocol.Command as Command+import Hercules.Agent.Worker.Effect (runEffect)+import Hercules.Agent.Worker.HerculesStore (nixStore, setBuilderCallback, withHerculesStore)+import Hercules.Agent.Worker.HerculesStore.Context (HerculesStore) import Hercules.Agent.WorkerProtocol.Command ( Command, )+import qualified Hercules.Agent.WorkerProtocol.Command as Command import qualified Hercules.Agent.WorkerProtocol.Command.Build as Build import qualified Hercules.Agent.WorkerProtocol.Command.BuildResult as BuildResult-import qualified Hercules.Agent.WorkerProtocol.Command.Eval as Eval+import qualified Hercules.Agent.WorkerProtocol.Command.Effect as Effect import Hercules.Agent.WorkerProtocol.Command.Eval ( Eval, )-import qualified Hercules.Agent.WorkerProtocol.Event as Event+import qualified Hercules.Agent.WorkerProtocol.Command.Eval as Eval import Hercules.Agent.WorkerProtocol.Event ( Event (Exception), )+import qualified Hercules.Agent.WorkerProtocol.Event as Event import qualified Hercules.Agent.WorkerProtocol.Event.Attribute as Attribute import qualified Hercules.Agent.WorkerProtocol.Event.AttributeError as AttributeError import qualified Hercules.Agent.WorkerProtocol.LogSettings as LogSettings+import Hercules.CNix as CNix+import Hercules.CNix.Expr (Match (IsAttrs, IsString), NixAttrs, RawValue, autoCallFunction, evalArgs, evalFile, getAttrBool, getAttrList, getAttrs, getDrvFile, getRecurseForDerivations, getStringIgnoreContext, init, isDerivation, isFunctor, match, rawValueType, withEvalStateConduit)+import Hercules.CNix.Expr.Context (EvalState)+import qualified Hercules.CNix.Expr.Raw+import Hercules.CNix.Expr.Typed (Value) import Hercules.Error import Katip import qualified Language.C.Inline.Cpp.Exceptions as C@@ -62,26 +71,25 @@ import qualified System.Environment as Environment import System.IO (BufferMode (LineBuffering), hSetBuffering) import System.Posix.IO (dup, fdToHandle, stdError)+import System.Posix.Signals (Handler (Catch), installHandler, raiseSignal, sigINT, sigTERM) import System.Timeout (timeout) import UnliftIO.Async (wait, withAsync) import UnliftIO.Exception (bracket, catch) import Prelude () import qualified Prelude -data HerculesState- = HerculesState- { drvsCompleted :: TVar (Map Text (UUID, BuildResult.BuildStatus)),- drvsInProgress :: IORef (Set Text),- herculesStore :: Ptr (Ref HerculesStore),- wrappedStore :: Ptr (Ref NixStore),- shortcutChannel :: Chan (Maybe Event)- }+data HerculesState = HerculesState+ { drvsCompleted :: TVar (Map Text (UUID, BuildResult.BuildStatus)),+ drvsInProgress :: IORef (Set Text),+ herculesStore :: Ptr (Ref HerculesStore),+ wrappedStore :: Store,+ shortcutChannel :: Chan (Maybe Event)+ } -data BuildException- = BuildException- { buildExceptionDerivationPath :: Text,- buildExceptionDetail :: Maybe Text- }+data BuildException = BuildException+ { buildExceptionDerivationPath :: Text,+ buildExceptionDetail :: Maybe Text+ } deriving (Show, Typeable) instance Exception BuildException@@ -89,7 +97,8 @@ main :: IO () main = do hSetBuffering stderr LineBuffering- CNix.init+ Hercules.CNix.Expr.init+ _ <- installHandler sigTERM (Catch $ raiseSignal sigINT) Nothing Logger.initLogger [options] <- Environment.getArgs let allOptions =@@ -109,25 +118,27 @@ withStore $ \wrappedStore_ -> withHerculesStore wrappedStore_ $ \herculesStore_ -> withKatip $ do liftIO $ setBuilderCallback herculesStore_ mempty ch <- liftIO newChan- let st = HerculesState- { drvsCompleted = drvsCompleted_,- drvsInProgress = drvsInProgress_,- herculesStore = herculesStore_,- wrappedStore = wrappedStore_,- shortcutChannel = ch- }+ let st =+ HerculesState+ { drvsCompleted = drvsCompleted_,+ drvsInProgress = drvsInProgress_,+ herculesStore = herculesStore_,+ wrappedStore = wrappedStore_,+ shortcutChannel = ch+ } let runner :: KatipContextT IO () runner = ( ( do- command <- runConduitRes -- Res shouldn't be necessary- ( transPipe liftIO (sourceHandle stdin)- .| conduitDecode- .| printCommands- .| await- )- >>= \case- Just c -> pure c- Nothing -> panic "Not a valid starting command"+ command <-+ runConduitRes -- Res shouldn't be necessary+ ( transPipe liftIO (sourceHandle stdin)+ .| conduitDecode+ .| printCommands+ .| await+ )+ >>= \case+ Just c -> pure c+ Nothing -> panic "Not a valid starting command" runCommand st ch command ) `safeLiftedCatch` ( \e -> liftIO $ do@@ -146,7 +157,7 @@ .| concatMapC (\x -> [Chunk x, Flush]) .| transPipe liftIO (sinkHandleFlush stdout) )- void $ do+ void $ withAsync runner $ \runnerAsync -> do writer -- runner can stop writer only by passing Nothing in channel (finally) logLocM DebugS "Writer done"@@ -156,7 +167,7 @@ printCommands = mapMC ( \x -> do- katipAddContext (sl "command" (show x :: Text)) $ do+ katipAddContext (sl "command" (show x :: Text)) $ logLocM DebugS "Received command" pure x )@@ -187,40 +198,51 @@ -- runCommand' :: HerculesState -> Command -> ConduitM Command Event (ResourceT IO) () runCommand herculesState ch command = do -- TODO don't do this- mainThread <- liftIO $ myThreadId+ mainThread <- liftIO myThreadId UnliftIO unlift <- askUnliftIO case command of- Command.Eval eval -> Logger.withLoggerConduit (logger $ Eval.logSettings eval) $ Logger.withTappedStderr Logger.tapper $ connectCommand ch $ do- void $ liftIO- $ flip- forkFinally- ( \eeu -> case eeu of- Left e -> throwIO $ FatalError $ "Failed to fork: " <> show e- Right _ -> pure ()- )- $ unlift- $ runConduitRes- ( Data.Conduit.handleC- ( \e -> do- yield $ Event.Error (renderException e)- liftIO $ throwTo mainThread e- )- ( do- runEval herculesState eval- liftIO $ throwTo mainThread ExitSuccess- )- .| sinkChanTerminate (shortcutChannel herculesState)- )- awaitForever $ \case- Command.BuildResult (BuildResult.BuildResult path attempt result) -> do- katipAddContext (sl "path" path <> sl "result" (show result :: Text))- $ logLocM DebugS- $ "Received remote build result"- liftIO $ atomically $ modifyTVar (drvsCompleted herculesState) (M.insert path (attempt, result))- _ -> pass+ Command.Eval eval -> Logger.withLoggerConduit (logger $ Eval.logSettings eval) $+ Logger.withTappedStderr Logger.tapper $+ connectCommand ch $ do+ void $+ liftIO $+ flip+ forkFinally+ (escalateAs \e -> FatalError $ "Failed to fork: " <> show e)+ $ unlift $+ runConduitRes+ ( Data.Conduit.handleC+ ( \e -> do+ yield $ Event.Error (renderException e)+ liftIO $ throwTo mainThread e+ )+ ( do+ runEval herculesState eval+ liftIO $ throwTo mainThread ExitSuccess+ )+ .| sinkChanTerminate (shortcutChannel herculesState)+ )+ awaitForever $ \case+ Command.BuildResult (BuildResult.BuildResult path attempt result) -> do+ katipAddContext (sl "path" path <> sl "result" (show result :: Text)) $+ logLocM+ DebugS+ "Received remote build result"+ liftIO $ atomically $ modifyTVar (drvsCompleted herculesState) (M.insert path (attempt, result))+ _ -> pass Command.Build build ->- Logger.withLoggerConduit (logger $ Build.logSettings build) $ Logger.withTappedStderr Logger.tapper $ do- connectCommand ch $ runBuild (wrappedStore herculesState) build+ katipAddNamespace "Build" $+ Logger.withLoggerConduit (logger $ Build.logSettings build) $+ Logger.withTappedStderr Logger.tapper $+ connectCommand ch $ runBuild (wrappedStore herculesState) build+ Command.Effect effect ->+ katipAddNamespace "Effect" $+ Logger.withLoggerConduit (logger $ Effect.logSettings effect) $+ Logger.withTappedStderr Logger.tapper $+ connectCommand ch $ do+ runEffect (wrappedStore herculesState) effect >>= \case+ ExitSuccess -> yield $ Event.EffectResult 0+ ExitFailure n -> yield $ Event.EffectResult n _ -> panic "Not a valid starting command" @@ -236,7 +258,7 @@ atomically $ Socket.write socket ping ) (const m)- Socket.withReliableSocket socketConfig $ \socket -> withPings socket $ katipAddNamespace "Build" do+ Socket.withReliableSocket socketConfig $ \socket -> withPings socket do let conduit = entriesSource .| Logger.unbatch@@ -246,21 +268,21 @@ .| socketSink socket batch = Logger.batch .| mapC (LogMessage.LogEntries . V.fromList) batchAndEnd =- ( (foldMapTap (Last . ims) `fuseUpstream` batch) >>= \case- Last (Just (i, ms)) -> yield $ LogMessage.End {i = i + 1, ms = ms}- Last Nothing -> yield $ LogMessage.End 0 0- )+ (foldMapTap (Last . ims) `fuseUpstream` batch) >>= \case+ Last (Just (i, ms)) -> yield $ LogMessage.End {i = i + 1, ms = ms}+ Last Nothing -> yield $ LogMessage.End 0 0 where ims (Chunk logEntry) = Just (LogEntry.i logEntry, LogEntry.ms logEntry) ims _ = Nothing- renumber i = await >>= traverse_ \case- Flush -> yield Flush >> renumber i- Chunk e -> do- yield $ Chunk e {LogEntry.i = i}- renumber (i + 1)- runConduit $ conduit+ renumber i =+ await >>= traverse_ \case+ Flush -> yield Flush >> renumber i+ Chunk e -> do+ yield $ Chunk e {LogEntry.i = i}+ renumber (i + 1)+ runConduit conduit logLocM DebugS "Syncing"- liftIO (timeout 600_000_000 $ Socket.syncIO $ socket) >>= \case+ liftIO (timeout 600_000_000 $ Socket.syncIO socket) >>= \case Just _ -> pass Nothing -> panic "Could not push logs within 10 minutes after completion" logLocM DebugS "Logger done"@@ -274,11 +296,12 @@ foldMapTap :: (Monoid b, Monad m) => (a -> b) -> ConduitT a a m b foldMapTap f = go mempty where- go b = await >>= \case- Nothing -> pure b- Just a -> do- yield a- go (b <> f a)+ go b =+ await >>= \case+ Nothing -> pure b+ Just a -> do+ yield a+ go (b <> f a) withKatip :: (MonadUnliftIO m) => KatipContextT m a -> m a withKatip m = do@@ -291,7 +314,7 @@ let makeLogEnv = registerScribe "stderr" handleScribe defaultScribeSettings =<< initLogEnv "Worker" "production" initialContext = () extraNs = mempty -- "Worker" is already set in initLogEnv.- -- closeScribes will stop accepting new logs, flush existing ones and clean up resources+ -- closeScribes will stop accepting new logs, flush existing ones and clean up resources bracket (liftIO makeLogEnv) (liftIO . closeScribes) $ \logEnv -> runKatipContextT logEnv initialContext extraNs m @@ -300,13 +323,14 @@ baseURL <- case Network.URI.parseURI $ toS $ LogSettings.baseURL l of Just x -> pure x Nothing -> panic "LogSettings: invalid base url"- pure Socket.SocketConfig- { makeHello = pure (LogMessage.LogEntries mempty),- checkVersion = Socket.checkVersion',- baseURL = baseURL,- path = LogSettings.path l,- token = encodeUtf8 $ LogSettings.reveal $ LogSettings.token l- }+ pure+ Socket.SocketConfig+ { makeHello = pure (LogMessage.LogEntries mempty),+ checkVersion = Socket.checkVersion',+ baseURL = baseURL,+ path = LogSettings.path l,+ token = encodeUtf8 $ reveal $ LogSettings.token l+ } -- TODO: test autoArgArgs :: Map Text Eval.Arg -> [ByteString]@@ -321,13 +345,16 @@ bracket acquire release . const where acquire =- liftIO $ join $ atomicModifyIORef ref $ \inprg ->- if drvPath `S.member` inprg- then (inprg, throwIO $ FatalError "Refusing to build derivation that should have been built remotely. Presumably, substitution has failed.")- else (S.insert drvPath inprg, pass)+ liftIO $+ join $+ atomicModifyIORef ref $ \inprg ->+ if drvPath `S.member` inprg+ then (inprg, throwIO $ FatalError "Refusing to build derivation that should have been built remotely. Presumably, substitution has failed.")+ else (S.insert drvPath inprg, pass) release _ =- liftIO $ atomicModifyIORef ref $ \inprg ->- (S.delete drvPath inprg, ())+ liftIO $+ atomicModifyIORef ref $ \inprg ->+ (S.delete drvPath inprg, ()) anyAlternative :: (Foldable l, Alternative f) => l a -> f a anyAlternative = getAlt . foldMap (Alt . pure)@@ -335,22 +362,26 @@ yieldAttributeError :: Monad m => [ByteString] -> SomeException -> ConduitT i Event m () yieldAttributeError path e | (Just e') <- fromException e =- yield $ Event.AttributeError $ AttributeError.AttributeError- { AttributeError.path = path,- AttributeError.message =- "Could not build derivation " <> buildExceptionDerivationPath e'- <> ", which is required during evaluation."- <> foldMap (" " <>) (buildExceptionDetail e'),- AttributeError.errorDerivation = Just (buildExceptionDerivationPath e'),- AttributeError.errorType = Just "BuildException"- }+ yield $+ Event.AttributeError $+ AttributeError.AttributeError+ { AttributeError.path = path,+ AttributeError.message =+ "Could not build derivation " <> buildExceptionDerivationPath e'+ <> ", which is required during evaluation."+ <> foldMap (" " <>) (buildExceptionDetail e'),+ AttributeError.errorDerivation = Just (buildExceptionDerivationPath e'),+ AttributeError.errorType = Just "BuildException"+ } yieldAttributeError path e =- yield $ Event.AttributeError $ AttributeError.AttributeError- { AttributeError.path = path,- AttributeError.message = renderException e,- AttributeError.errorDerivation = Nothing,- AttributeError.errorType = Just (show (typeOf e))- }+ yield $+ Event.AttributeError $+ AttributeError.AttributeError+ { AttributeError.path = path,+ AttributeError.message = renderException e,+ AttributeError.errorDerivation = Nothing,+ AttributeError.errorType = Just (show (typeOf e))+ } maybeThrowBuildException :: MonadIO m => BuildResult.BuildStatus -> Text -> m () maybeThrowBuildException result plainDrvText =@@ -372,51 +403,56 @@ s <- storeUri store UnliftIO unlift <- lift askUnliftIO let decode = decodeUtf8With lenientDecode- liftIO $ setBuilderCallback hStore $ \path -> unlift $ katipAddContext (sl "fullpath" (decode path)) $ do- logLocM DebugS "Building"- let (plainDrv, bangOut) = BS.span (/= fromIntegral (ord '!')) path- outputName = BS.dropWhile (== fromIntegral (ord '!')) bangOut- plainDrvText = decode plainDrv- withDrvInProgress st plainDrvText $ do- liftIO $ writeChan shortcutChan $ Just $ Event.Build plainDrvText (decode outputName) Nothing- derivation <- liftIO $ getDerivation store plainDrv- outputPath <- liftIO $ getDerivationOutputPath derivation outputName- katipAddContext (sl "outputPath" (decode outputPath)) $ do- logLocM DebugS "Naively calling ensurePath"- liftIO (ensurePath (wrappedStore st) outputPath) `catch` \e0 -> do- katipAddContext (sl "message" (show (e0 :: SomeException) :: Text)) $- logLocM DebugS "Recovering from failed wrapped.ensurePath"- (attempt0, result) <-- liftIO $ atomically $ do- c <- readTVar drvsCompl- anyAlternative $ M.lookup plainDrvText c- liftIO $ maybeThrowBuildException result plainDrvText- liftIO $ clearSubstituterCaches- liftIO $ clearPathInfoCache store- liftIO (ensurePath (wrappedStore st) outputPath) `catch` \e1 -> do- katipAddContext (sl "message" (show (e1 :: SomeException) :: Text)) $- logLocM DebugS "Recovering from fresh ensurePath"- liftIO $ writeChan shortcutChan $ Just $ Event.Build plainDrvText (decode outputName) (Just attempt0)- -- TODO sync- result' <-- liftIO $ atomically $ do- c <- readTVar drvsCompl- (attempt1, r) <- anyAlternative $ M.lookup plainDrvText c- guard (attempt1 /= attempt0)- pure r- liftIO $ maybeThrowBuildException result' plainDrvText- liftIO $ clearSubstituterCaches- liftIO $ clearPathInfoCache store- liftIO (ensurePath (wrappedStore st) outputPath) `catch` \e2 -> do- liftIO $ throwIO $- BuildException- plainDrvText- ( Just $- "It could not be retrieved on the evaluating agent, despite a successful rebuild. Exception: "- <> show (e2 :: SomeException)- )- logLocM DebugS ("Built")- withEvalState store $ \evalState -> do+ liftIO $+ setBuilderCallback hStore $ \path -> unlift $+ katipAddContext (sl "fullpath" (decode path)) $ do+ logLocM DebugS "Building"+ let (plainDrv, bangOut) = BS.span (/= fromIntegral (ord '!')) path+ outputName = BS.dropWhile (== fromIntegral (ord '!')) bangOut+ plainDrvText = decode plainDrv+ withDrvInProgress st plainDrvText $ do+ liftIO $ writeChan shortcutChan $ Just $ Event.Build plainDrvText (decode outputName) Nothing+ derivation <- liftIO $ getDerivation store plainDrv+ outputPath <- liftIO $ getDerivationOutputPath derivation outputName+ katipAddContext (sl "outputPath" (decode outputPath)) $+ logLocM DebugS "Naively calling ensurePath"+ liftIO (ensurePath (wrappedStore st) outputPath) `catch` \e0 -> do+ katipAddContext (sl "message" (show (e0 :: SomeException) :: Text)) $+ logLocM DebugS "Recovering from failed wrapped.ensurePath"+ (attempt0, result) <-+ liftIO $+ atomically $ do+ c <- readTVar drvsCompl+ anyAlternative $ M.lookup plainDrvText c+ liftIO $ maybeThrowBuildException result plainDrvText+ liftIO clearSubstituterCaches+ liftIO $ clearPathInfoCache store+ liftIO (ensurePath (wrappedStore st) outputPath) `catch` \e1 -> do+ katipAddContext (sl "message" (show (e1 :: SomeException) :: Text)) $+ logLocM DebugS "Recovering from fresh ensurePath"+ liftIO $ writeChan shortcutChan $ Just $ Event.Build plainDrvText (decode outputName) (Just attempt0)+ -- TODO sync+ result' <-+ liftIO $+ atomically $ do+ c <- readTVar drvsCompl+ (attempt1, r) <- anyAlternative $ M.lookup plainDrvText c+ guard (attempt1 /= attempt0)+ pure r+ liftIO $ maybeThrowBuildException result' plainDrvText+ liftIO clearSubstituterCaches+ liftIO $ clearPathInfoCache store+ liftIO (ensurePath (wrappedStore st) outputPath) `catch` \e2 ->+ liftIO $+ throwIO $+ BuildException+ plainDrvText+ ( Just $+ "It could not be retrieved on the evaluating agent, despite a successful rebuild. Exception: "+ <> show (e2 :: SomeException)+ )+ logLocM DebugS "Built"+ withEvalStateConduit store $ \evalState -> do katipAddContext (sl "storeURI" (decode s)) $ logLocM DebugS "EvalState loaded." args <-@@ -432,7 +468,7 @@ walk :: (MonadUnliftIO m, KatipContext m) => Ptr EvalState ->- Bindings ->+ Value NixAttrs -> RawValue -> ConduitT i Event m () walk evalState = walk' True [] 10@@ -447,7 +483,7 @@ -- | Depth of tree remaining Integer -> -- | Auto arguments to pass to (attrset-)functions- Bindings ->+ Value NixAttrs -> -- | Current node of the walk RawValue -> -- | Program that performs the walk and emits 'Event's@@ -465,16 +501,50 @@ if isDeriv then do drvPath <- getDrvFile evalState v- yield $- Event.Attribute Attribute.Attribute- { Attribute.path = path,- Attribute.drv = drvPath- }+ typE <- runExceptT do+ isEffect <- liftEitherAs Left =<< liftIO (getAttrBool evalState attrValue "isEffect")+ case isEffect of+ Just True -> throwE $ Right Attribute.Effect+ _ -> pass+ isDependenciesOnly <- liftEitherAs Left =<< liftIO (getAttrBool evalState attrValue "buildDependenciesOnly")+ case isDependenciesOnly of+ Just True -> throwE $ Right Attribute.DependenciesOnly+ _ -> pass+ phases <- liftEitherAs Left =<< liftIO (getAttrList evalState attrValue "phases")+ case phases of+ Just [aSingularPhase] ->+ liftIO (match evalState aSingularPhase) >>= liftEitherAs Left >>= \case+ IsString phaseNameValue -> do+ phaseName <- liftIO (getStringIgnoreContext phaseNameValue)+ when (phaseName == "nobuildPhase") do+ throwE $ Right Attribute.DependenciesOnly+ _ -> pass+ _ -> pass+ mayFail <- liftEitherAs Left =<< liftIO (getAttrBool evalState attrValue "ignoreFailure")+ case mayFail of+ Just True -> throwE $ Right Attribute.MayFail+ _ -> pass+ mustFail <- liftEitherAs Left =<< liftIO (getAttrBool evalState attrValue "requireFailure")+ case mustFail of+ Just True -> throwE $ Right Attribute.MustFail+ _ -> pass+ let yieldAttribute typ =+ yield $+ Event.Attribute+ Attribute.Attribute+ { Attribute.path = path,+ Attribute.drv = drvPath,+ Attribute.typ = typ+ }+ case typE of+ Left (Left e) -> yieldAttributeError path e+ Left (Right t) -> yieldAttribute t+ Right _ -> yieldAttribute Attribute.Regular else do walkAttrset <- if forceWalkAttrset then pure True- else-- Hydra doesn't seem to obey this, because it walks the+ else -- Hydra doesn't seem to obey this, because it walks the -- x64_64-linux etc attributes per package. Maybe those -- are special cases? -- For now, we will assume that people don't build a whole Nixpkgs@@ -486,28 +556,34 @@ walk' True path (depthRemaining - 1) autoArgs x else do attrs <- liftIO $ getAttrs attrValue- void- $ flip M.traverseWithKey attrs- $ \name value ->- when (depthRemaining > 0 && walkAttrset) $- walk' -- TODO: else warn- False- (path ++ [name])- (depthRemaining - 1)- autoArgs- value+ void $+ flip M.traverseWithKey attrs $+ \name value ->+ when (depthRemaining > 0 && walkAttrset) $+ walk' -- TODO: else warn+ False+ (path ++ [name])+ (depthRemaining - 1)+ autoArgs+ value _any -> do vt <- liftIO $ rawValueType v unless ( lastMay path == Just "recurseForDerivations" && vt- == CNix.Internal.Raw.Bool+ == Hercules.CNix.Expr.Raw.Bool )- $ logLocM DebugS- $ logStr- $ "Ignoring "- <> show path- <> " : "- <> (show vt :: Text)+ $ logLocM DebugS $+ logStr $+ "Ignoring "+ <> show path+ <> " : "+ <> (show vt :: Text) pass++liftEitherAs :: MonadError e m => (e0 -> e) -> Either e0 a -> m a+liftEitherAs f = liftEither . rmap+ where+ rmap (Left e) = Left (f e)+ rmap (Right a) = Right a
hercules-ci-agent-worker/Hercules/Agent/Worker/Build.hs view
@@ -3,9 +3,6 @@ module Hercules.Agent.Worker.Build where -import CNix-import CNix.Internal.Context (Derivation)-import Cachix.Client.Store (Store, queryPathInfo, validPathInfoNarHash, validPathInfoNarSize) import Conduit import Data.Conduit.Katip.Orphans () import Foreign (ForeignPtr)@@ -15,11 +12,17 @@ import Hercules.Agent.WorkerProtocol.Event (Event) import qualified Hercules.Agent.WorkerProtocol.Event as Event import qualified Hercules.Agent.WorkerProtocol.Event.BuildResult as Event.BuildResult+import Hercules.CNix+ ( DerivationOutput (derivationOutputName, derivationOutputPath),+ getDerivationOutputs,+ )+import qualified Hercules.CNix as CNix+import Hercules.CNix.Store (Store, queryPathInfo, validPathInfoNarHash, validPathInfoNarSize)+import Hercules.CNix.Store.Context (Derivation) import Katip import Protolude hiding (yield)-import Unsafe.Coerce -runBuild :: (MonadIO m, KatipContext m) => Ptr (Ref NixStore) -> Command.Build.Build -> ConduitT i Event m ()+runBuild :: (MonadIO m, KatipContext m) => Store -> Command.Build.Build -> ConduitT i Event m () runBuild store build = do let extraPaths = Command.Build.inputDerivationOutputPaths build drvPath = encodeUtf8 $ Command.Build.drvPath build@@ -31,7 +34,7 @@ pure $ Command.Build.materializeDerivation build Left (e :: SomeException) -> liftIO do CNix.logInfo $ "while retrieving dependencies: " <> toS (displayException e)- CNix.logInfo $ "unable to retrieve dependency; will build locally"+ CNix.logInfo "unable to retrieve dependency; attempting fallback to local build" pure True derivationMaybe <- liftIO $ Build.getDerivation store drvPath derivation <- case derivationMaybe of@@ -44,23 +47,21 @@ yield $ Event.BuildResult buildResult -- TODO: case distinction on BuildStatus enumeration-enrichResult :: Ptr (Ref NixStore) -> ForeignPtr Derivation -> Build.BuildResult -> IO Event.BuildResult.BuildResult-enrichResult _ _ result@Build.BuildResult {isSuccess = False} = pure $- Event.BuildResult.BuildFailure {errorMessage = Build.errorMessage result}+enrichResult :: Store -> ForeignPtr Derivation -> Build.BuildResult -> IO Event.BuildResult.BuildResult+enrichResult _ _ result@Build.BuildResult {isSuccess = False} =+ pure $+ Event.BuildResult.BuildFailure {errorMessage = Build.errorMessage result} enrichResult store derivation _ = do drvOuts <- getDerivationOutputs derivation outputInfos <- for drvOuts $ \drvOut -> do- vpi <- queryPathInfo (coerceStore store) (derivationOutputPath drvOut)+ vpi <- queryPathInfo store (derivationOutputPath drvOut) hash_ <- validPathInfoNarHash vpi let size = validPathInfoNarSize vpi- pure Event.BuildResult.OutputInfo- { name = derivationOutputName drvOut,- path = derivationOutputPath drvOut,- hash = hash_,- size = size- }+ pure+ Event.BuildResult.OutputInfo+ { name = derivationOutputName drvOut,+ path = derivationOutputPath drvOut,+ hash = hash_,+ size = size+ } pure $ Event.BuildResult.BuildSuccess outputInfos---- TODO factor out cnix library and avoid unsafeCoerce https://github.com/hercules-ci/hercules-ci-agent/issues/223-coerceStore :: Ptr (Ref NixStore) -> Store-coerceStore = unsafeCoerce
hercules-ci-agent-worker/Hercules/Agent/Worker/Build/Logger.hs view
@@ -5,7 +5,6 @@ module Hercules.Agent.Worker.Build.Logger where -import CNix.Internal.Context import Conduit (MonadUnliftIO, filterC) import qualified Data.ByteString.Char8 as BSC import Data.ByteString.Unsafe (unsafePackMallocCString)@@ -15,6 +14,8 @@ import Foreign (alloca, nullPtr, peek) import Hercules.API.Logs.LogEntry (LogEntry) import qualified Hercules.API.Logs.LogEntry as LogEntry+import Hercules.Agent.Worker.Build.Logger.Context (Fields, HerculesLoggerEntry, context)+import Hercules.CNix.Store.Context (unsafeMallocBS) import Katip import qualified Language.C.Inline.Cpp as C import qualified Language.C.Inline.Cpp.Exceptions as C@@ -123,7 +124,7 @@ forNonNull p f = if p == nullPtr then pure Nothing else Just <$> f p -- popping multiple lines into an array would be nice-convertEntry :: Ptr HerculesLoggerEntry -> IO (LogEntry)+convertEntry :: Ptr HerculesLoggerEntry -> IO LogEntry convertEntry logEntryPtr = alloca \millisPtr -> alloca \textStrPtr -> alloca \levelPtr -> alloca \activityIdPtr -> alloca \typePtr -> alloca \parentPtr -> alloca \fieldsPtrPtr -> do r <-@@ -162,12 +163,13 @@ textStr <- peek textStrPtr text_ <- unsafePackMallocCString textStr level_ <- peek levelPtr- pure $ LogEntry.Msg- { i = i_,- ms = ms_,- level = fromIntegral level_,- msg = decode text_- }+ pure $+ LogEntry.Msg+ { i = i_,+ ms = ms_,+ level = fromIntegral level_,+ msg = decode text_+ } 2 -> do text_ <- unsafePackMallocCString =<< peek textStrPtr act_ <- peek activityIdPtr@@ -175,41 +177,43 @@ parent_ <- peek parentPtr typ_ <- peek typePtr fields_ <- convertAndDeleteFields =<< peek fieldsPtrPtr- pure $ LogEntry.Start- { i = i_,- ms = ms_,- act = LogEntry.ActivityId act_,- level = fromIntegral level_,- typ = LogEntry.ActivityType typ_,- text = decode text_,- parent = LogEntry.ActivityId parent_,- fields = fields_- }+ pure $+ LogEntry.Start+ { i = i_,+ ms = ms_,+ act = LogEntry.ActivityId act_,+ level = fromIntegral level_,+ typ = LogEntry.ActivityType typ_,+ text = decode text_,+ parent = LogEntry.ActivityId parent_,+ fields = fields_+ } 3 -> do act_ <- peek activityIdPtr- pure $ LogEntry.Stop- { i = i_,- ms = ms_,- act = LogEntry.ActivityId act_- }+ pure $+ LogEntry.Stop+ { i = i_,+ ms = ms_,+ act = LogEntry.ActivityId act_+ } 4 -> do act_ <- peek activityIdPtr typ_ <- peek typePtr fields_ <- convertAndDeleteFields =<< peek fieldsPtrPtr- pure $ LogEntry.Result- { i = i_,- ms = ms_,- act = LogEntry.ActivityId act_,- rtype = LogEntry.ResultType typ_,- fields = fields_- }+ pure $+ LogEntry.Result+ { i = i_,+ ms = ms_,+ act = LogEntry.ActivityId act_,+ rtype = LogEntry.ResultType typ_,+ fields = fields_+ } _ -> panic "convertEntry invalid internal type" convertAndDeleteFields :: Ptr Fields -> IO (Vector LogEntry.Field) convertAndDeleteFields fieldsPtr = flip finally- ( [C.block| void { delete $(LoggerFields *fieldsPtr); }|]- )+ [C.block| void { delete $(LoggerFields *fieldsPtr); }|] do size <- [C.exp| size_t { $(LoggerFields *fieldsPtr)->size() }|] V.generateM (fromIntegral size) $ \i' ->@@ -251,22 +255,23 @@ withLoggerConduit logger io = withAsync (logger popper) $ \popperAsync -> ((io `finally` liftIO close) <* wait popperAsync) `onException` liftIO (timeout 2_000_000 (wait popperAsync)) where- popper = liftIO popMany >>= \case- lns | null lns -> pass- lns -> do- yield lns- popper+ popper =+ liftIO popMany >>= \case+ lns | null lns -> pass+ lns -> do+ yield lns+ popper -- | Remove spammy progress results. Use 'nubProgress' instead? filterProgress :: Monad m => ConduitT (Flush LogEntry) (Flush LogEntry) m () filterProgress = filterC \case- Chunk (LogEntry.Result {rtype = LogEntry.ResultTypeProgress}) -> False+ Chunk LogEntry.Result {rtype = LogEntry.ResultTypeProgress} -> False _ -> True nubProgress :: Monad m => ConduitT (Flush LogEntry) (Flush LogEntry) m () nubProgress = nubSubset (toChunk >=> toProgressKey) where- toProgressKey k@(LogEntry.Result {rtype = LogEntry.ResultTypeProgress}) = Just k {LogEntry.i = 0}+ toProgressKey k@LogEntry.Result {rtype = LogEntry.ResultTypeProgress} = Just k {LogEntry.i = 0} toProgressKey _ = Nothing toChunk (Chunk a) = Just a toChunk Flush = Nothing@@ -279,37 +284,41 @@ batch :: Monad m => ConduitT (Flush a) [a] m () batch = go [] where- go acc = await >>= \case- Nothing -> do- unless (null acc) (yield $ reverse acc)- Just Flush -> do- unless (null acc) (yield $ reverse acc)- go []- Just (Chunk c) -> do- go (c : acc)+ go acc =+ await >>= \case+ Nothing -> do+ unless (null acc) (yield $ reverse acc)+ Just Flush -> do+ unless (null acc) (yield $ reverse acc)+ go []+ Just (Chunk c) -> do+ go (c : acc) nubSubset :: (Eq k, Monad m) => (a -> Maybe k) -> ConduitT a a m ()-nubSubset toKey = await >>= \case- Nothing -> pass- Just firstA -> yield firstA- >> case toKey firstA of- Nothing -> nubSubset toKey- Just firstK -> nubSubset1 toKey firstK+nubSubset toKey =+ await >>= \case+ Nothing -> pass+ Just firstA ->+ yield firstA+ >> case toKey firstA of+ Nothing -> nubSubset toKey+ Just firstK -> nubSubset1 toKey firstK nubSubset1 :: (Eq k, Monad m) => (a -> Maybe k) -> k -> ConduitT a a m ()-nubSubset1 toKey prevKey = await >>= \case- Nothing -> pass- Just a -> case toKey a of- Nothing -> do- yield a- nubSubset1 toKey prevKey- Just ak -> do- unless (ak == prevKey) do+nubSubset1 toKey prevKey =+ await >>= \case+ Nothing -> pass+ Just a -> case toKey a of+ Nothing -> do yield a- nubSubset1 toKey ak+ nubSubset1 toKey prevKey+ Just ak -> do+ unless (ak == prevKey) do+ yield a+ nubSubset1 toKey ak tryReadLine :: MonadUnliftIO m => Handle -> m (Either () ByteString)-tryReadLine s = tryJust (\e -> guard $ isEOFError e) (liftIO (BSC.hGetLine s))+tryReadLine s = tryJust (guard . isEOFError) (liftIO (BSC.hGetLine s)) tapper :: (KatipContext m, MonadUnliftIO m) => TapState -> m () tapper s = do@@ -317,9 +326,6 @@ Left _ -> pass Right "__%%hercules terminate log%%__" -> pass Right ln -> do- katipAddContext (sl "line" (decode ln))- $ logLocM DebugS- $ "Intercepted stderr" liftIO [C.throwBlock| void { std::string s = $bs-cstr:ln;@@ -374,7 +380,8 @@ hSetBuffering h LineBuffering pure h readableHandle <- mkHandle readable- pure TapState- { originalStderrCopy = oldStdError,- readableStderrEnd = readableHandle- }+ pure+ TapState+ { originalStderrCopy = oldStdError,+ readableStderrEnd = readableHandle+ }
+ hercules-ci-agent-worker/Hercules/Agent/Worker/Build/Logger/Context.hs view
@@ -0,0 +1,35 @@+{-# LANGUAGE EmptyDataDecls #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}++module Hercules.Agent.Worker.Build.Logger.Context where++import qualified Data.Map as M+import qualified Language.C.Inline.Context as C+import qualified Language.C.Inline.Cpp as C+import qualified Language.C.Types as C+import Protolude++data HerculesLoggerEntry++data Fields++data LogEntryQueue++context :: C.Context+context =+ C.cppCtx <> C.fptrCtx+ <> C.bsCtx+ <> loggerContext++(=:) :: k -> a -> Map k a+(=:) = M.singleton++loggerContext :: C.Context+loggerContext =+ mempty+ { C.ctxTypesTable =+ C.TypeName "HerculesLoggerEntry" =: [t|HerculesLoggerEntry|]+ <> C.TypeName "LogEntryQueue" =: [t|LogEntryQueue|]+ <> C.TypeName "LoggerFields" =: [t|Fields|]+ }
hercules-ci-agent-worker/Hercules/Agent/Worker/Build/Prefetched.hs view
@@ -9,11 +9,10 @@ module Hercules.Agent.Worker.Build.Prefetched where -import CNix-import CNix.Internal.Context import qualified Data.ByteString.Char8 as C8 import Foreign (FinalizerPtr, ForeignPtr, alloca, newForeignPtr, nullPtr, peek) import Foreign.C (peekCString)+import Hercules.CNix.Store import qualified Language.C.Inline.Cpp as C import qualified Language.C.Inline.Cpp.Exceptions as C import Protolude@@ -38,7 +37,7 @@ C.include "<nix/fs-accessor.hh>" -C.include "hercules-aliases.h"+C.include "<hercules-ci-cnix/store.hxx>" C.using "namespace nix" @@ -78,25 +77,23 @@ toBuildStatus (-1) = Successful toBuildStatus _ = UnknownFailure -data BuildResult- = BuildResult- { isSuccess :: Bool,- status :: BuildStatus,- startTime :: C.CTime,- stopTime :: C.CTime,- errorMessage :: Text- }+data BuildResult = BuildResult+ { isSuccess :: Bool,+ status :: BuildStatus,+ startTime :: C.CTime,+ stopTime :: C.CTime,+ errorMessage :: Text+ } deriving (Show) nullableForeignPtr :: FinalizerPtr a -> Ptr a -> IO (Maybe (ForeignPtr a)) nullableForeignPtr _ rawPtr | rawPtr == nullPtr = pure Nothing nullableForeignPtr finalize rawPtr = Just <$> newForeignPtr finalize rawPtr -getDerivation :: Ptr (Ref NixStore) -> ByteString -> IO (Maybe (ForeignPtr Derivation))-getDerivation store derivationPath =+getDerivation :: Store -> ByteString -> IO (Maybe (ForeignPtr Derivation))+getDerivation (Store store) derivationPath = nullableForeignPtr finalizeDerivation =<< [C.throwBlock| Derivation *{- Store &store = **$(refStore* store); std::string derivationPath($bs-ptr:derivationPath, $bs-len:derivationPath); std::list<nix::ref<nix::Store>> stores = getDefaultSubstituters(); stores.push_front(*$(refStore* store));@@ -129,8 +126,8 @@ }|] -- | @buildDerivation derivationPath derivationText@-buildDerivation :: Ptr (Ref NixStore) -> ByteString -> ForeignPtr Derivation -> Maybe [ByteString] -> IO BuildResult-buildDerivation store derivationPath derivation extraInputs =+buildDerivation :: Store -> ByteString -> ForeignPtr Derivation -> Maybe [ByteString] -> IO BuildResult+buildDerivation (Store store) derivationPath derivation extraInputs = let extraInputsMerged = C8.intercalate "\n" (fromMaybe [] extraInputs) materializeDerivation = if isNothing extraInputs then 1 else 0 in alloca $ \successPtr ->@@ -235,10 +232,11 @@ stopTimeValue <- peek stopTimePtr errorMessageValue0 <- peek errorMessagePtr errorMessageValue <- peekCString errorMessageValue0- pure $ BuildResult- { isSuccess = successValue /= 0,- status = toBuildStatus statusValue,- startTime = startTimeValue,- stopTime = stopTimeValue,- errorMessage = toS errorMessageValue- }+ pure $+ BuildResult+ { isSuccess = successValue /= 0,+ status = toBuildStatus statusValue,+ startTime = startTimeValue,+ stopTime = stopTimeValue,+ errorMessage = toS errorMessageValue+ }
+ hercules-ci-agent-worker/Hercules/Agent/Worker/Effect.hs view
@@ -0,0 +1,49 @@+{-# LANGUAGE BlockArguments #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Hercules.Agent.Worker.Effect where++import Control.Monad.Catch (MonadThrow)+import GHC.ForeignPtr (ForeignPtr)+import Hercules.Agent.Worker.Build.Prefetched (buildDerivation)+import qualified Hercules.Agent.Worker.Build.Prefetched as Build+import qualified Hercules.Agent.WorkerProtocol.Command.Effect as Command.Effect+import Hercules.CNix (Store)+import qualified Hercules.CNix as CNix+import Hercules.CNix.Store.Context (Derivation)+import qualified Hercules.Effect as Effect+import Katip (KatipContext)+import Protolude+import UnliftIO.Directory (getCurrentDirectory)++runEffect :: (MonadIO m, KatipContext m, MonadThrow m) => Store -> Command.Effect.Effect -> m ExitCode+runEffect store command = do+ derivation <- prepareDerivation store command+ dir <- getCurrentDirectory+ Effect.runEffect derivation (Command.Effect.token command) (Command.Effect.secretsPath command) (Command.Effect.apiBaseURL command) dir++prepareDerivation :: MonadIO m => Store -> Command.Effect.Effect -> m (ForeignPtr Derivation)+prepareDerivation store command = do+ let extraPaths = Command.Effect.inputDerivationOutputPaths command+ drvPath = encodeUtf8 $ Command.Effect.drvPath command+ ensureDeps = for_ extraPaths $ \input ->+ liftIO $ CNix.ensurePath store input+ liftIO $ do+ ensureDeps `catch` \e -> do+ CNix.logInfo $ "while retrieving dependencies: " <> toS (displayException (e :: SomeException))+ CNix.logInfo "unable to retrieve dependency; attempting fallback to local build"+ CNix.ensurePath store drvPath+ derivation <- CNix.getDerivation store drvPath+ depDrvPaths <- CNix.getDerivationInputs derivation+ for_ depDrvPaths \(depDrv, _outputs) -> do+ depDerivation <- CNix.getDerivation store depDrv+ _nixBuildResult <- liftIO $ buildDerivation store depDrv depDerivation mempty+ pass+ derivation <-+ liftIO (Build.getDerivation store drvPath) >>= \case+ Just drv -> pure drv+ Nothing -> panic $ "Could not retrieve derivation " <> show drvPath <> " from local store or binary caches."+ sources <- liftIO $ CNix.getDerivationSources derivation+ for_ sources \src -> do+ liftIO $ CNix.ensurePath store src+ pure derivation
+ hercules-ci-agent-worker/Hercules/Agent/Worker/HerculesStore.hs view
@@ -0,0 +1,102 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}++module Hercules.Agent.Worker.HerculesStore+ ( module Hercules.Agent.Worker.HerculesStore,+ HerculesStore,+ )+where++import Control.Exception+ ( catch,+ )+import qualified Data.ByteString.Unsafe as BS+import Data.Coerce (coerce)+import qualified Foreign.C+import Foreign.C.String (withCString)+import Foreign.StablePtr (castStablePtrToPtr, newStablePtr)+import Hercules.Agent.Worker.HerculesStore.Context+ ( ExceptionPtr,+ HerculesStore,+ context,+ )+import Hercules.CNix (Store)+import Hercules.CNix.Expr (Store (Store))+import Hercules.CNix.Store.Context (Ref)+import qualified Language.C.Inline.Cpp as C+import qualified Language.C.Inline.Cpp.Exceptions as C+import Protolude+import Prelude ()++C.context context++C.include "<cstring>"++C.include "<nix/config.h>"++C.include "<nix/shared.hh>"++C.include "<nix/store-api.hh>"++C.include "<nix/get-drvs.hh>"++C.include "<nix/derivations.hh>"++C.include "<nix/affinity.hh>"++C.include "<nix/globals.hh>"++C.include "hercules-aliases.h"++C.using "namespace nix"++withHerculesStore ::+ Store ->+ (Ptr (Ref HerculesStore) -> IO a) ->+ IO a+withHerculesStore (Store wrappedStore) =+ bracket+ ( liftIO+ [C.block| refHerculesStore* {+ refStore &s = *$(refStore *wrappedStore);+ refHerculesStore hs(new HerculesStore({}, s));+ return new refHerculesStore(hs);+ } |]+ )+ (\x -> liftIO [C.exp| void { delete $(refHerculesStore* x) } |])++nixStore :: Ptr (Ref HerculesStore) -> Store+nixStore = coerce++printDiagnostics :: Ptr (Ref HerculesStore) -> IO ()+printDiagnostics s =+ [C.throwBlock| void{+ (*$(refHerculesStore* s))->printDiagnostics();+ }|]++-- TODO catch pure exceptions from displayException+setBuilderCallback :: Ptr (Ref HerculesStore) -> (ByteString -> IO ()) -> IO ()+setBuilderCallback s callback = do+ p <-+ mkBuilderCallback $ \cstr exceptionToThrowPtr ->+ Control.Exception.catch (BS.unsafePackMallocCString cstr >>= callback) $ \e ->+ withCString (displayException (e :: SomeException)) $ \renderedException -> do+ stablePtr <- castStablePtrToPtr <$> newStablePtr e+ [C.block| void {+ (*$(exception_ptr *exceptionToThrowPtr)) = std::make_exception_ptr(HaskellException(std::string($(const char* renderedException)), $(void* stablePtr)));+ }|]+ [C.throwBlock| void {+ (*$(refHerculesStore* s))->setBuilderCallback($(void (*p)(const char *, exception_ptr *) ));+ }|]++type BuilderCallback = Foreign.C.CString -> Ptr ExceptionPtr -> IO ()++-- Work around a problem in ghcide with foreign imports.+#ifndef __GHCIDE__+foreign import ccall "wrapper"+ mkBuilderCallback :: BuilderCallback -> IO (FunPtr BuilderCallback)+#else+mkBuilderCallback :: BuilderCallback -> IO (FunPtr BuilderCallback)+mkBuilderCallback = panic "This is a stub to work around a ghcide issue. Please compile without -D__GHCIDE__"+#endif
+ hercules-ci-agent-worker/Hercules/Agent/Worker/HerculesStore/Context.hs view
@@ -0,0 +1,35 @@+{-# LANGUAGE EmptyDataDecls #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}++module Hercules.Agent.Worker.HerculesStore.Context where++import qualified Data.Map as M+import Hercules.CNix.Expr.Context (Ref)+import qualified Hercules.CNix.Store.Context as Store+import qualified Language.C.Inline.Context as C+import qualified Language.C.Inline.Cpp as C+import qualified Language.C.Types as C+import Protolude++data HerculesStore++data ExceptionPtr++context :: C.Context+context =+ C.cppCtx <> C.fptrCtx+ <> C.bsCtx+ <> Store.context+ <> herculesStoreContext++(=:) :: k -> a -> Map k a+(=:) = M.singleton++herculesStoreContext :: C.Context+herculesStoreContext =+ mempty+ { C.ctxTypesTable =+ C.TypeName "refHerculesStore" =: [t|Ref HerculesStore|]+ <> C.TypeName "exception_ptr" =: [t|ExceptionPtr|]+ }
hercules-ci-agent.cabal view
@@ -1,7 +1,7 @@ cabal-version: 2.4 name: hercules-ci-agent-version: 0.7.5+version: 0.8.0 synopsis: Runs Continuous Integration tasks on your machines category: Nix, CI, Testing, DevOps homepage: https://docs.hercules-ci.com@@ -49,18 +49,23 @@ exposed-modules: Data.Fixed.Extras Data.Time.Extras+ Hercules.Agent.NixFile Hercules.Agent.Producer+ Hercules.Agent.Sensitive Hercules.Agent.Socket Hercules.Agent.STM Hercules.Agent.WorkerProtocol.Command Hercules.Agent.WorkerProtocol.Command.Build Hercules.Agent.WorkerProtocol.Command.BuildResult+ Hercules.Agent.WorkerProtocol.Command.Effect Hercules.Agent.WorkerProtocol.Command.Eval Hercules.Agent.WorkerProtocol.Event Hercules.Agent.WorkerProtocol.Event.Attribute Hercules.Agent.WorkerProtocol.Event.AttributeError Hercules.Agent.WorkerProtocol.Event.BuildResult Hercules.Agent.WorkerProtocol.LogSettings+ Hercules.Effect+ Hercules.Effect.Container Data.Conduit.Extras Data.Conduit.Katip.Orphans @@ -81,24 +86,32 @@ , bytestring , conduit , containers+ , directory , dlist , exceptions+ , filepath , hercules-ci-api-agent+ , hercules-ci-api-core+ , hercules-ci-cnix-store , katip+ , lens+ , lens-aeson , lifted-async , lifted-base , monad-control , mtl , network-uri- , optparse-applicative , protolude >= 0.3 , process+ , process-extras , safe-exceptions , stm+ , temporary , text , time , transformers-base , unbounded-delays+ , unix , unliftio-core , unliftio , uuid@@ -106,57 +119,6 @@ , wuss default-language: Haskell2010 --- ugly hack to avoid https://gitlab.haskell.org/ghc/ghc/issues/16477-library internal-ffi- exposed-modules:- Hercules.Agent.StoreFFI- hs-source-dirs: src-internal-ffi- build-depends: base, protolude- default-language: Haskell2010--library cnix- import: cxx-opts- exposed-modules:- CNix- CNix.Internal.Context- CNix.Internal.Raw- CNix.Internal.Store- CNix.Internal.Typed- hs-source-dirs: src-cnix- include-dirs:- cbits- cxx-sources:- cbits/hercules-store.cxx- cbits/hercules-logger.cxx- build-depends:- base- , inline-c- , inline-c-cpp- , internal-ffi- , bytestring- , cachix >= 0.5.1- , conduit- , containers- , protolude- , unliftio-core- pkgconfig-depends:- nix-store >= 2.0- , nix-expr >= 2.0- , nix-main >= 2.0- , bdw-gc- extra-libraries:- boost_context- default-language: Haskell2010- default-extensions: DeriveGeneric DeriveTraversable DisambiguateRecordFields FlexibleContexts InstanceSigs LambdaCase MultiParamTypeClasses NoImplicitPrelude OverloadedStrings RankNTypes TupleSections TypeApplications TypeOperators- ghc-options:- -Werror=incomplete-patterns -Werror=missing-fields- -Wall- -fwarn-tabs- -fwarn-unused-imports- -fwarn-missing-signatures- -fwarn-name-shadowing- -fwarn-incomplete-patterns- executable hercules-ci-agent main-is: Main.hs other-modules:@@ -176,10 +138,12 @@ Hercules.Agent.Config Hercules.Agent.Config.BinaryCaches Hercules.Agent.Compat+ Hercules.Agent.Effect Hercules.Agent.Env Hercules.Agent.EnvironmentInfo Hercules.Agent.Evaluate Hercules.Agent.Evaluate.TraversalQueue+ Hercules.Agent.Files Hercules.Agent.Init Hercules.Agent.Log Hercules.Agent.Nix@@ -203,14 +167,14 @@ aeson , async , attoparsec- , base >=4.7 && <5+ , base , base64-bytestring , binary , binary-conduit , bytestring , cachix , cachix-api- , cnix+ , hercules-ci-cnix-store , conduit , conduit-extra , containers@@ -219,8 +183,8 @@ , exceptions , filepath , hercules-ci-agent- , hercules-ci-api-core == 0.1.1.0- , hercules-ci-api-agent == 0.2.2.0+ , hercules-ci-api-core == 0.1.2.0+ , hercules-ci-api-agent == 0.3.0.0 , hostname , http-client , http-client-tls@@ -236,6 +200,7 @@ , network , optparse-applicative , process+ , process-extras , protolude , safe-exceptions , servant >=0.14.1@@ -266,11 +231,18 @@ Hercules.Agent.Worker.Build Hercules.Agent.Worker.Build.Prefetched Hercules.Agent.Worker.Build.Logger+ Hercules.Agent.Worker.Build.Logger.Context+ Hercules.Agent.Worker.Effect Paths_hercules_ci_agent+ Hercules.Agent.Worker.HerculesStore+ Hercules.Agent.Worker.HerculesStore.Context autogen-modules: Paths_hercules_ci_agent hs-source-dirs: hercules-ci-agent-worker+ cxx-sources:+ cbits/hercules-store.cxx+ cbits/hercules-logger.cxx default-extensions: DeriveGeneric DeriveTraversable DisambiguateRecordFields FlexibleContexts InstanceSigs LambdaCase MultiParamTypeClasses NoImplicitPrelude OverloadedStrings RankNTypes TupleSections TypeApplications TypeOperators ghc-options: -Werror=incomplete-patterns -Werror=missing-fields@@ -290,31 +262,39 @@ build-depends: aeson , async- , base >=4.7 && <5+ , base , binary , binary-conduit , bytestring , cachix- , cnix , conduit , containers+ , directory , exceptions+ , filepath , hercules-ci-agent , hercules-ci-api-agent , hercules-ci-api-core- , internal-ffi+ , hercules-ci-cnix-store+ , hercules-ci-cnix-expr , inline-c , inline-c-cpp , katip+ , lens+ , lens-aeson , lifted-async , lifted-base , monad-control+ , mtl , network-uri- , optparse-applicative , protolude+ , process+ , process-extras , safe-exceptions , stm , text+ , temporary+ , transformers , transformers-base , unix , unliftio@@ -350,11 +330,11 @@ aeson , async , attoparsec- , base >=4.7 && <5+ , base , binary , binary-conduit , bytestring- , cnix+ , hercules-ci-cnix-store , conduit , containers , exceptions@@ -367,7 +347,6 @@ , lifted-async , lifted-base , monad-control- , optparse-applicative , process , protolude , safe-exceptions
hercules-ci-agent/Data/Functor/Partitioner.hs view
@@ -20,13 +20,12 @@ import Protolude -- TODO: Profunctor-data Partitioner a b- = forall m.- (Monoid m) =>- Partitioner- { ingest :: a -> Maybe m,- digest :: m -> b- }+data Partitioner a b = forall m.+ (Monoid m) =>+ Partitioner+ { ingest :: a -> Maybe m,+ digest :: m -> b+ } partitionList :: Partitioner a b -> [a] -> b partitionList (Partitioner ing dig) = dig . fold . mapMaybe ing@@ -42,16 +41,16 @@ fmap f (Partitioner ing dig) = (Partitioner ing (f . dig)) instance Applicative (Partitioner a) where- pure a = Partitioner {ingest = const (Nothing :: Maybe ()), digest = pure a} - (Partitioner ingp digp) <*> (Partitioner ingq digq) = Partitioner- { ingest = \a -> case ingp a of- Just mp -> Just (mp, mempty)- Nothing -> (mempty,) <$> ingq a,- digest = \(mp, mq) -> digp mp (digq mq)- }+ (Partitioner ingp digp) <*> (Partitioner ingq digq) =+ Partitioner+ { ingest = \a -> case ingp a of+ Just mp -> Just (mp, mempty)+ Nothing -> (mempty,) <$> ingq a,+ digest = \(mp, mq) -> digp mp (digq mq)+ } -- instance Profunctor Partitioner -- ...@@ -71,22 +70,24 @@ Ord k => (k -> v -> Maybe a) -> Partitioner (WithKey k v) (Map k a)-partWithKey f = Partitioner- { ingest = \(WithKey (k, v)) -> M.singleton k <$> f k v,- digest = identity- }+partWithKey f =+ Partitioner+ { ingest = \(WithKey (k, v)) -> M.singleton k <$> f k v,+ digest = identity+ } traversePartWithKey :: (Ord k, Applicative f) => (k -> v -> Maybe (f a)) -> Partitioner (WithKey k v) (f (Map k a))-traversePartWithKey f = Partitioner- { ingest = \(WithKey (k, v)) -> do- -- Maybe- x <- f k v- pure $ Ap $ (M.singleton k <$> x),- digest = getAp- }+traversePartWithKey f =+ Partitioner+ { ingest = \(WithKey (k, v)) -> do+ -- Maybe+ x <- f k v+ pure $ Ap $ (M.singleton k <$> x),+ digest = getAp+ } -- | Use with 'partWithKey' to match on the key. --
hercules-ci-agent/Hercules/Agent.hs view
@@ -10,9 +10,7 @@ withAsync, ) import Control.Concurrent.Lifted (forkFinally, killThread)-import Control.Concurrent.STM (TVar, readTVar) import Control.Concurrent.STM.TChan-import Control.Exception (displayException) import Control.Exception.Lifted (bracket) import Control.Monad.IO.Unlift (MonadUnliftIO) import qualified Data.Aeson as A@@ -20,10 +18,11 @@ import Data.Time (getCurrentTime) import qualified Data.UUID.V4 as UUID import qualified Hercules.API.Agent.Build.BuildTask as BuildTask+import qualified Hercules.API.Agent.Effect.EffectTask as EffectTask import qualified Hercules.API.Agent.Evaluate.EvaluateTask as EvaluateTask import qualified Hercules.API.Agent.LifeCycle as LifeCycle-import qualified Hercules.API.Agent.LifeCycle.StartInfo as StartInfo import Hercules.API.Agent.LifeCycle.StartInfo (tasksInProgress)+import qualified Hercules.API.Agent.LifeCycle.StartInfo as StartInfo import qualified Hercules.API.Agent.Socket.AgentPayload as AgentPayload import qualified Hercules.API.Agent.Socket.ServicePayload as ServicePayload import Hercules.API.Agent.Tasks@@ -43,11 +42,12 @@ tasksClient, ) import qualified Hercules.Agent.Config as Config-import qualified Hercules.Agent.Env as Env+import qualified Hercules.Agent.Effect as Effect import Hercules.Agent.Env ( App, runHerculesClient, )+import qualified Hercules.Agent.Env as Env import Hercules.Agent.EnvironmentInfo (extractAgentInfo) import qualified Hercules.Agent.Evaluate as Evaluate import qualified Hercules.Agent.Init as Init@@ -55,7 +55,6 @@ import qualified Hercules.Agent.Options as Options import Hercules.Agent.STM import Hercules.Agent.Socket as Socket-import Hercules.Agent.Socket (serviceChan) import Hercules.Agent.Token (withAgentToken) import Hercules.Error ( cap,@@ -103,28 +102,32 @@ run :: Env.Env -> Config.FinalConfig -> IO () run env _cfg = do- Env.runApp env- $ katipAddContext (sl "agent-version" (A.String herculesAgentVersion))- $ (configureLimits >>)- $ (configChecks >>)- $ withAgentToken- $ withLifeCycle \hello -> withTaskState \tasks ->- withAgentSocket hello tasks \socket ->- withApplicationLevelPinger socket $ do- logLocM InfoS "Agent online."- forever $ do- (liftIO $ atomically $ readTChan $ serviceChan socket) >>= \case- ServicePayload.ServiceInfo _ -> pass- ServicePayload.StartEvaluation evalTask ->- launchTask tasks socket (Task.upcastId $ EvaluateTask.id evalTask) do- Cache.withCaches do- Evaluate.performEvaluation evalTask- pure $ TaskStatus.Successful ()- ServicePayload.StartBuild buildTask ->- launchTask tasks socket (Task.upcastId $ BuildTask.id buildTask) do- Cache.withCaches $- Build.performBuild buildTask- ServicePayload.Cancel cancellation -> cancelTask tasks socket cancellation+ Env.runApp env $+ katipAddContext (sl "agent-version" (A.String herculesAgentVersion)) $+ (configureLimits >>) $+ (configChecks >>) $+ withAgentToken $+ withLifeCycle \hello -> withTaskState \tasks ->+ withAgentSocket hello tasks \socket ->+ withApplicationLevelPinger socket $ do+ logLocM InfoS "Agent online."+ forever $ do+ liftIO (atomically $ readTChan $ serviceChan socket) >>= \case+ ServicePayload.ServiceInfo _ -> pass+ ServicePayload.StartEvaluation evalTask ->+ launchTask tasks socket (Task.upcastId $ EvaluateTask.id evalTask) do+ Cache.withCaches do+ Evaluate.performEvaluation evalTask+ pure $ TaskStatus.Successful ()+ ServicePayload.StartBuild buildTask ->+ launchTask tasks socket (Task.upcastId $ BuildTask.id buildTask) do+ Cache.withCaches $+ Build.performBuild buildTask+ ServicePayload.StartEffect effectTask ->+ launchTask tasks socket (Task.upcastId $ EffectTask.id effectTask) do+ Cache.withCaches $+ Effect.performEffect effectTask+ ServicePayload.Cancel cancellation -> cancelTask tasks socket cancellation withTaskState :: (TVar (Map (Id (Task Task.Any)) ThreadId) -> App a) -> App a withTaskState f = do@@ -176,10 +179,10 @@ report $ TaskStatus.Exceptional $ toS $ displayException e -- TODO use socket report status =- retry (cap 60 exponential)- $ noContent- $ runHerculesClient- $ tasksSetStatus tasksClient taskId status+ retry (cap 60 exponential) $+ noContent $+ runHerculesClient $+ tasksSetStatus tasksClient taskId status void @_ @ThreadId $ flip forkFinally done do insertSelf@@ -206,16 +209,16 @@ now <- liftIO getCurrentTime freshId <- Id <$> liftIO UUID.nextRandom let startInfo = StartInfo.StartInfo {id = freshId, startTime = now}- hello = StartInfo.Hello- { agentInfo = agentInfo,- startInfo = startInfo,- tasksInProgress = []- }+ hello =+ StartInfo.Hello+ { agentInfo = agentInfo,+ startInfo = startInfo,+ tasksInProgress = []+ } req r =- retry (cap 60 exponential)- $ noContent- $ runHerculesClient- $ r+ retry (cap 60 exponential) $+ noContent $+ runHerculesClient r sayGoodbye = req $ LifeCycle.goodbye lifeCycleClient startInfo bracket pass (\() -> sayGoodbye) (\() -> app hello) @@ -234,10 +237,11 @@ tryIncreaseResourceLimitTo :: (MonadUnliftIO m, KatipContext m) => Resource -> Text -> ResourceLimit -> m () tryIncreaseResourceLimitTo resource resourceName target = katipAddContext (sl "resource" resourceName) do lims <- liftIO $ getResourceLimit resource- let lims' = ResourceLimits- { softLimit = target & atLeast (softLimit lims) & atMost (hardLimit lims),- hardLimit = hardLimit lims- }+ let lims' =+ ResourceLimits+ { softLimit = target & atLeast (softLimit lims) & atMost (hardLimit lims),+ hardLimit = hardLimit lims+ } atLeast ResourceLimitInfinity _ = ResourceLimitInfinity atLeast _ ResourceLimitInfinity = ResourceLimitInfinity atLeast ResourceLimitUnknown r = r@@ -250,8 +254,8 @@ atMost l ResourceLimitInfinity = l atMost ResourceLimitUnknown r = r -- atMost l ResourceLimitUnknown = l -- already covered- showLimit (ResourceLimitInfinity) = "infinity"- showLimit (ResourceLimitUnknown) = A.Null+ showLimit ResourceLimitInfinity = "infinity"+ showLimit ResourceLimitUnknown = A.Null showLimit (ResourceLimit n) = A.Number $ fromIntegral n liftIO (setResourceLimit resource lims') `catch` \e -> katipAddContext
hercules-ci-agent/Hercules/Agent/AgentSocket.hs view
@@ -2,10 +2,10 @@ import qualified Data.Map as M import Hercules.API.Agent.LifeCycle.StartInfo (Hello, tasksInProgress)-import qualified Hercules.API.Agent.Socket.AgentPayload as AgentPayload import Hercules.API.Agent.Socket.AgentPayload (AgentPayload)-import qualified Hercules.API.Agent.Socket.ServicePayload as ServicePayload+import qualified Hercules.API.Agent.Socket.AgentPayload as AgentPayload import Hercules.API.Agent.Socket.ServicePayload (ServicePayload)+import qualified Hercules.API.Agent.Socket.ServicePayload as ServicePayload import Hercules.API.Id (Id) import Hercules.API.Task (Task) import qualified Hercules.API.Task as Task
hercules-ci-agent/Hercules/Agent/Build.hs view
@@ -4,15 +4,15 @@ import qualified Data.Map as M import qualified Hercules.API.Agent.Build as API.Build import qualified Hercules.API.Agent.Build.BuildEvent as BuildEvent-import qualified Hercules.API.Agent.Build.BuildEvent.OutputInfo as OutputInfo import Hercules.API.Agent.Build.BuildEvent.OutputInfo ( OutputInfo, )+import qualified Hercules.API.Agent.Build.BuildEvent.OutputInfo as OutputInfo import qualified Hercules.API.Agent.Build.BuildEvent.Pushed as Pushed-import qualified Hercules.API.Agent.Build.BuildTask as BuildTask import Hercules.API.Agent.Build.BuildTask ( BuildTask, )+import qualified Hercules.API.Agent.Build.BuildTask as BuildTask import Hercules.API.Servant (noContent) import Hercules.API.TaskStatus (TaskStatus) import qualified Hercules.API.TaskStatus as TaskStatus@@ -23,6 +23,7 @@ import qualified Hercules.Agent.Env as Env import Hercules.Agent.Log import qualified Hercules.Agent.Nix as Nix+import Hercules.Agent.Sensitive (Sensitive (Sensitive)) import qualified Hercules.Agent.ServiceInfo as ServiceInfo import Hercules.Agent.WorkerProcess import qualified Hercules.Agent.WorkerProcess as WorkerProcess@@ -43,7 +44,7 @@ statusRef <- newIORef Nothing extraNixOptions <- Nix.askExtraOptions workerEnv <- liftIO $ WorkerProcess.prepareEnv (WorkerProcess.WorkerEnvSettings {nixPath = mempty})- let opts = [show $ extraNixOptions]+ let opts = [show extraNixOptions] procSpec = (System.Process.proc workerExe opts) { env = Just workerEnv,@@ -59,16 +60,21 @@ _ -> pass baseURL <- asks (ServiceInfo.bulkSocketBaseURL . Env.serviceInfo) materialize <- asks (not . Config.nixUserIsTrusted . Env.config)- liftIO $ writeChan commandChan $ Just $ Command.Build $ Command.Build.Build- { drvPath = BuildTask.derivationPath buildTask,- inputDerivationOutputPaths = encodeUtf8 <$> BuildTask.inputDerivationOutputPaths buildTask,- logSettings = LogSettings.LogSettings- { token = LogSettings.Sensitive $ BuildTask.logToken buildTask,- path = "/api/v1/logs/build/socket",- baseURL = toS $ Network.URI.uriToString identity baseURL ""- },- materializeDerivation = materialize- }+ liftIO $+ writeChan commandChan $+ Just $+ Command.Build $+ Command.Build.Build+ { drvPath = BuildTask.derivationPath buildTask,+ inputDerivationOutputPaths = encodeUtf8 <$> BuildTask.inputDerivationOutputPaths buildTask,+ logSettings =+ LogSettings.LogSettings+ { token = Sensitive $ BuildTask.logToken buildTask,+ path = "/api/v1/logs/build/socket",+ baseURL = toS $ Network.URI.uriToString identity baseURL ""+ },+ materializeDerivation = materialize+ } exitCode <- runWorker procSpec (stderrLineHandler "Builder") commandChan writeEvent logLocM DebugS $ "Worker exit: " <> logStr (show exitCode :: Text) case exitCode of@@ -76,13 +82,13 @@ _ -> panic $ "Worker failed: " <> show exitCode status <- readIORef statusRef case status of- Just (BuildResult.BuildSuccess {outputs = outs'}) -> do+ Just BuildResult.BuildSuccess {outputs = outs'} -> do let outs = convertOutputs (BuildTask.derivationPath buildTask) outs' reportOutputInfos buildTask outs push buildTask outs reportSuccess buildTask pure $ TaskStatus.Successful ()- Just (BuildResult.BuildFailure {}) -> pure $ TaskStatus.Terminated ()+ Just BuildResult.BuildFailure {} -> pure $ TaskStatus.Terminated () Nothing -> pure $ TaskStatus.Exceptional "Build did not complete" convertOutputs :: Text -> [BuildResult.OutputInfo] -> Map Text OutputInfo
hercules-ci-agent/Hercules/Agent/Cache.hs view
@@ -2,8 +2,6 @@ module Hercules.Agent.Cache where -import qualified CNix-import qualified Cachix.Client.Store as Store import qualified Data.Map as M import qualified Data.Text as T import qualified Data.Text.IO as T@@ -14,15 +12,16 @@ import qualified Hercules.Agent.Env as Env import qualified Hercules.Agent.Nix as Nix import qualified Hercules.Agent.SecureDirectory as SecureDirectory+import qualified Hercules.CNix as CNix+import qualified Hercules.CNix.Store as Store import qualified Hercules.Formats.NixCache as NixCache import Katip import Protolude import System.IO (hClose)-import qualified Unsafe.Coerce withCaches :: App a -> App a withCaches m = do- netrcLns <- Cachix.getNetrcLines+ netrcLns <- (<>) <$> Cachix.getNetrcLines <*> Nix.getNetrcLines csubsts <- Cachix.getSubstituters cpubkeys <- Cachix.getTrustedPublicKeys nixCaches <- asks (Config.nixCaches . Env.binaryCaches)@@ -81,12 +80,9 @@ Store.addToPathSet path pathSet pure pathSet -castStore :: Ptr (CNix.Ref CNix.NixStore) -> Store.Store-castStore = Unsafe.Coerce.unsafeCoerce--signClosure :: Ptr (CNix.Ref CNix.NixStore) -> ForeignPtr CNix.SecretKey -> Store.PathSet -> IO (Sum Int, Sum Int)+signClosure :: CNix.Store -> ForeignPtr CNix.SecretKey -> Store.PathSet -> IO (Sum Int, Sum Int) signClosure store key' pathSet = withForeignPtr key' \key -> do- closure <- Store.computeFSClosure (castStore store) Store.defaultClosureParams pathSet+ closure <- Store.computeFSClosure store Store.defaultClosureParams pathSet closure & Store.traversePathSet ( \path -> do
hercules-ci-agent/Hercules/Agent/Cachix.hs view
@@ -23,36 +23,38 @@ nixStore = nixStore, clientEnv = clientEnv } <-- asks $ Agent.Cachix.getEnv+ asks Agent.Cachix.getEnv pushCache <-- escalate- $ maybeToEither (FatalError $ "Cache not found " <> cache)- $ M.lookup cache pushCaches+ escalate $+ maybeToEither (FatalError $ "Cache not found " <> cache) $+ M.lookup cache pushCaches ul <- askUnliftIO+ let pushParams =+ Cachix.Push.PushParams+ { pushParamsName = Agent.Cachix.pushCacheName pushCache,+ pushParamsSecret = Agent.Cachix.pushCacheSecret pushCache,+ pushParamsStore = nixStore,+ pushParamsClientEnv = clientEnv,+ pushParamsStrategy = \storePath ->+ let ctx = withNamedContext "path" storePath+ in Cachix.Push.PushStrategy+ { onAlreadyPresent = pass,+ onAttempt = \retryStatus size ->+ ctx $+ withNamedContext "size" size $+ withNamedContext "retry" (show retryStatus :: Text) $+ logLocM DebugS "pushing",+ on401 = throwIO $ FatalError "Cachix push is unauthorized",+ onError = \err -> throwIO $ FatalError $ "Error pushing to cachix: " <> show err,+ onDone = ctx $ logLocM DebugS "push done",+ withXzipCompressor = Cachix.Push.defaultWithXzipCompressor,+ omitDeriver = False+ }+ } void $ Cachix.Push.pushClosure- ( \f l ->- liftIO $ Cachix.Push.mapConcurrentlyBounded workers (fmap (unliftIO ul) f) l- )- clientEnv- nixStore- pushCache- ( \storePath ->- let ctx = withNamedContext "path" storePath- in Cachix.Push.PushStrategy- { onAlreadyPresent = pass,- onAttempt = \retryStatus size ->- ctx- $ withNamedContext "size" size- $ withNamedContext "retry" (show retryStatus :: Text)- $ logLocM DebugS "pushing",- on401 = throwIO $ FatalError $ "Cachix push is unauthorized",- onError = \err -> throwIO $ FatalError $ "Error pushing to cachix: " <> show err,- onDone = ctx $ logLocM DebugS "push done",- withXzipCompressor = Cachix.Push.defaultWithXzipCompressor,- omitDeriver = False- }- )+ (\f l -> liftIO $ Cachix.Push.mapConcurrentlyBounded workers (fmap (unliftIO ul) f) l)+ pushParams paths getNetrcLines :: App [Text]
hercules-ci-agent/Hercules/Agent/Cachix/Env.hs view
@@ -1,19 +1,23 @@ module Hercules.Agent.Cachix.Env where import qualified Cachix.Client.Push as Cachix-import Cachix.Client.Store (Store)+import Hercules.CNix.Store (Store) import Hercules.Formats.CachixCache (CachixCache) import Protolude import Servant.Client (ClientEnv) -data Env- = Env- { pushCaches :: Map Text Cachix.PushCache,- cacheKeys :: Map Text CachixCache,- netrcLines :: [Text],- nixStore :: Store,- clientEnv :: ClientEnv- }+data PushCache = PushCache+ { pushCacheName :: Text,+ pushCacheSecret :: Cachix.PushSecret+ }++data Env = Env+ { pushCaches :: Map Text PushCache,+ cacheKeys :: Map Text CachixCache,+ netrcLines :: [Text],+ nixStore :: Store,+ clientEnv :: ClientEnv+ } class HasEnv env where getEnv :: env -> Env
hercules-ci-agent/Hercules/Agent/Cachix/Init.hs view
@@ -5,11 +5,11 @@ import Cachix.Client.Env (cachixVersion) import qualified Cachix.Client.Push as Cachix.Push import qualified Cachix.Client.Secrets as Cachix.Secrets-import qualified Cachix.Client.Store as Cachix.Store import Cachix.Client.URI (defaultCachixBaseUrl) import qualified Data.Map as M import Hercules.Agent.Cachix.Env as Env import qualified Hercules.Agent.Config as Config+import Hercules.CNix.Store (openStore) import Hercules.Error import qualified Hercules.Formats.CachixCache as CachixCache import qualified Katip as K@@ -35,15 +35,16 @@ K.katipAddContext (K.sl "caches" (M.keys cks)) $ K.logLocM K.DebugS ("Cachix init " <> K.logStr (show (M.keys cks) :: Text)) pcs <- liftIO $ toPushCaches cks- store <- liftIO Cachix.Store.openStore+ store <- liftIO openStore httpManager <- newTlsManagerWith customManagerSettings- pure Env.Env- { pushCaches = pcs,- netrcLines = toNetrcLines cks,- cacheKeys = cks,- nixStore = store,- clientEnv = mkClientEnv httpManager defaultCachixBaseUrl- }+ pure+ Env.Env+ { pushCaches = pcs,+ netrcLines = toNetrcLines cks,+ cacheKeys = cks,+ nixStore = store,+ clientEnv = mkClientEnv httpManager defaultCachixBaseUrl+ } toNetrcLines :: Map Text CachixCache.CachixCache -> [Text] toNetrcLines = concatMap toNetrcLine . M.toList@@ -52,19 +53,30 @@ pt <- toList $ CachixCache.authToken keys pure $ "machine " <> name <> ".cachix.org" <> " password " <> pt -toPushCaches :: Map Text CachixCache.CachixCache -> IO (Map Text Cachix.Push.PushCache)+toPushCaches :: Map Text CachixCache.CachixCache -> IO (Map Text PushCache) toPushCaches = sequenceA . M.mapMaybeWithKey toPushCaches' where toPushCaches' name keys = let t = fromMaybe "" (CachixCache.authToken keys) in do sk <- head $ CachixCache.signingKeys keys- Just $ escalateAs FatalError $ do- k' <- Cachix.Secrets.parseSigningKeyLenient sk- pure Cachix.Push.PushCache- { pushCacheName = name,- pushCacheSecret =- Cachix.Push.PushSigningKey- (Servant.Auth.Client.Token $ encodeUtf8 t)- k'- }+ Just $+ escalateAs FatalError $ do+ k' <- Cachix.Secrets.parseSigningKeyLenient sk+ pure+ PushCache+ { pushCacheName = name,+ pushCacheSecret =+ Cachix.Push.PushSigningKey+ (Servant.Auth.Client.Token $ encodeUtf8 t)+ k'+ }+ <|> do+ token <- head $ CachixCache.authToken keys+ Just $+ pure+ PushCache+ { pushCacheName = name,+ pushCacheSecret =+ Cachix.Push.PushToken (Servant.Auth.Client.Token $ encodeUtf8 token)+ }
hercules-ci-agent/Hercules/Agent/Client.hs view
@@ -1,5 +1,9 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE TypeApplications #-}+{-# LANGUAGE UndecidableInstances #-} {-# OPTIONS_GHC -O0 #-}+{-# OPTIONS_GHC -Wno-orphans #-} -- TODO https://github.com/haskell-servant/servant/issues/986 module Hercules.Agent.Client@@ -16,32 +20,47 @@ import Hercules.API.Agent.Build (BuildAPI) import Hercules.API.Agent.Evaluate (EvalAPI) import Hercules.API.Agent.LifeCycle (LifeCycleAPI)+import Hercules.API.Agent.State (ContentLength) import Hercules.API.Agent.Tasks (TasksAPI) import Hercules.API.Logs (LogsAPI) import Hercules.API.Servant (useApi) import Protolude+import Servant.API import Servant.API.Generic import Servant.Auth.Client ()-import qualified Servant.Client-import Servant.Client (ClientM) import Servant.Client.Generic (AsClientT)+import Servant.Client.Streaming (ClientM)+import qualified Servant.Client.Streaming +-- | Bad instance to make it the client for State api compile. GHC seems to pick+-- the wrong overlappable instance.+instance+ FromSourceIO+ ByteString+ ( Headers+ '[Hercules.API.Agent.State.ContentLength]+ (SourceIO ByteString)+ )+ where+ fromSourceIO = addHeader (-1) . fromSourceIO+ client :: AgentAPI ClientAuth (AsClientT ClientM)-client = fromServant $ Servant.Client.client (servantApi @ClientAuth)+client = fromServant $ Servant.Client.Streaming.client (servantApi @ClientAuth) tasksClient :: TasksAPI ClientAuth (AsClientT ClientM)-tasksClient = useApi tasks $ Hercules.Agent.Client.client+tasksClient = useApi tasks Hercules.Agent.Client.client evalClient :: EvalAPI ClientAuth (AsClientT ClientM)-evalClient = useApi eval $ Hercules.Agent.Client.client+evalClient = useApi eval Hercules.Agent.Client.client buildClient :: BuildAPI ClientAuth (AsClientT ClientM)-buildClient = useApi build $ Hercules.Agent.Client.client+buildClient = useApi build Hercules.Agent.Client.client lifeCycleClient :: LifeCycleAPI ClientAuth (AsClientT ClientM)-lifeCycleClient = useApi lifeCycle $ Hercules.Agent.Client.client+lifeCycleClient = useApi lifeCycle Hercules.Agent.Client.client logsClient :: LogsAPI () (AsClientT ClientM) logsClient =- fromServant $ Servant.Client.client $- (Proxy @(AddAPIVersion (ToServantApi (LogsAPI ()))))+ fromServant $+ Servant.Client.Streaming.client+ (Proxy @(AddAPIVersion (ToServantApi (LogsAPI ()))))
hercules-ci-agent/Hercules/Agent/Config.hs view
@@ -14,6 +14,7 @@ ) where +import GHC.Conc (getNumProcessors) import Katip (Severity (..)) import Protolude hiding (to) import qualified System.Environment@@ -28,28 +29,29 @@ data Purpose = Input | Final -- | Whether the 'Final' value is optional.-data Sort = Required | Optional+data Sort = Required | Optional | From Sort Type type family Item purpose sort a where+ Item 'Input ('From sort b) a = Item 'Input sort b+ Item 'Final ('From sort b) a = Item 'Final sort a Item 'Input _sort a = Maybe a Item 'Final 'Required a = a Item 'Final 'Optional a = Maybe a type FinalConfig = Config 'Final -data Config purpose- = Config- { herculesApiBaseURL :: Item purpose 'Required Text,- nixUserIsTrusted :: Item purpose 'Required Bool,- concurrentTasks :: Item purpose 'Required Integer,- baseDirectory :: Item purpose 'Required FilePath,- -- | Read-only- staticSecretsDirectory :: Item purpose 'Required FilePath,- workDirectory :: Item purpose 'Required FilePath,- clusterJoinTokenPath :: Item purpose 'Required FilePath,- binaryCachesPath :: Item purpose 'Required FilePath,- logLevel :: Item purpose 'Required Severity- }+data Config purpose = Config+ { herculesApiBaseURL :: Item purpose 'Required Text,+ nixUserIsTrusted :: Item purpose 'Required Bool,+ concurrentTasks :: Item purpose ('From 'Required (Either () Int)) Int,+ baseDirectory :: Item purpose 'Required FilePath,+ -- | Read-only+ staticSecretsDirectory :: Item purpose 'Required FilePath,+ workDirectory :: Item purpose 'Required FilePath,+ clusterJoinTokenPath :: Item purpose 'Required FilePath,+ binaryCachesPath :: Item purpose 'Required FilePath,+ logLevel :: Item purpose 'Required Severity+ } deriving (Generic) deriving instance Show (Config 'Final)@@ -61,7 +63,10 @@ .= herculesApiBaseURL <*> dioptional (Toml.bool "nixUserIsTrusted") .= nixUserIsTrusted- <*> dioptional (Toml.integer "concurrentTasks")+ <*> dioptional+ ( Toml.dimatch matchRight Right (Toml.int "concurrentTasks")+ <|> Toml.dimatch matchLeft Left (Toml.textBy (\() -> "auto") isAuto "concurrentTasks")+ ) .= concurrentTasks <*> dioptional (Toml.string keyBaseDirectory) .= baseDirectory@@ -76,6 +81,18 @@ <*> dioptional (Toml.enumBounded "logLevel") .= logLevel +matchLeft :: Either a b -> Maybe a+matchLeft (Left a) = Just a+matchLeft _ = Nothing++matchRight :: Either a1 a2 -> Maybe a2+matchRight (Right a) = Just a+matchRight _ = Nothing++isAuto :: Text -> Either Text ()+isAuto "auto" = Right ()+isAuto _ = Left "The only permissible string value is \"auto\""+ keyClusterJoinTokenPath :: Key keyClusterJoinTokenPath = "clusterJoinTokenPath" @@ -91,9 +108,6 @@ defaultApiBaseUrl :: Text defaultApiBaseUrl = "https://hercules-ci.com" -defaultConcurrentTasks :: Integer-defaultConcurrentTasks = 4- readConfig :: ConfigPath -> IO (Config 'Input) readConfig loc = case loc of TomlPath fp -> Toml.decodeFile tomlCodec (toS fp)@@ -116,20 +130,27 @@ (binaryCachesPath input) workDir = fromMaybe (baseDir </> "work") (workDirectory input) dabu <- determineDefaultApiBaseUrl- let rawConcurrentTasks = fromMaybe defaultConcurrentTasks $ concurrentTasks input+ numProc <- getNumProcessors+ let -- make sure the default is at least two, for ifd+ autoConcurrentTasks = max 2 numProc+ configuredConcurrentTasks = case concurrentTasks input of+ Nothing -> autoConcurrentTasks+ Just (Left _auto) -> autoConcurrentTasks+ Just (Right n) -> fromIntegral n validConcurrentTasks <-- case rawConcurrentTasks of- x | not (x >= 1) -> throwIO $ FatalError "concurrentTasks must be at least 1"- x -> pure x+ case configuredConcurrentTasks of+ x | x >= 1 -> pure x+ _ -> throwIO $ FatalError "concurrentTasks must be at least 1" let apiBaseUrl = fromMaybe dabu $ herculesApiBaseURL input- pure Config- { herculesApiBaseURL = apiBaseUrl,- nixUserIsTrusted = fromMaybe False $ nixUserIsTrusted input,- binaryCachesPath = binaryCachesP,- clusterJoinTokenPath = clusterJoinTokenP,- concurrentTasks = validConcurrentTasks,- baseDirectory = baseDir,- staticSecretsDirectory = staticSecretsDir,- workDirectory = workDir,- logLevel = logLevel input & fromMaybe InfoS- }+ pure+ Config+ { herculesApiBaseURL = apiBaseUrl,+ nixUserIsTrusted = fromMaybe False $ nixUserIsTrusted input,+ binaryCachesPath = binaryCachesP,+ clusterJoinTokenPath = clusterJoinTokenP,+ concurrentTasks = validConcurrentTasks,+ baseDirectory = baseDir,+ staticSecretsDirectory = staticSecretsDir,+ workDirectory = workDir,+ logLevel = logLevel input & fromMaybe InfoS+ }
hercules-ci-agent/Hercules/Agent/Config/BinaryCaches.hs view
@@ -20,12 +20,11 @@ import Protolude hiding (catchJust) import System.IO.Error (isDoesNotExistError) -data BinaryCaches- = BinaryCaches- { cachixCaches :: Map Text CachixCache,- nixCaches :: Map Text NixCache,- unknownKinds :: Map Text UnknownKind- }+data BinaryCaches = BinaryCaches+ { cachixCaches :: Map Text CachixCache,+ nixCaches :: Map Text NixCache,+ unknownKinds :: Map Text UnknownKind+ } data UnknownKind = UnknownKind {kind :: Text} deriving (Generic, FromJSON)@@ -35,8 +34,8 @@ parseJSON = parseBag ( BinaryCaches- <$> part (\_name -> whenKind "CachixCache" $ \v -> Just $ (parseJSON v))- <*> part (\_name -> whenKind "NixCache" $ \v -> Just $ (parseJSON v))+ <$> part (\_name -> whenKind "CachixCache" $ \v -> Just $ parseJSON v)+ <*> part (\_name -> whenKind "NixCache" $ \v -> Just $ parseJSON v) <*> part (\_name v -> Just $ parseJSON v) ) @@ -48,15 +47,17 @@ (mfilter isDoesNotExistError . Just) (liftIO (BL.readFile (toS fname))) ( \_e ->- liftIO $ throwIO $ FatalError $- "The binary-caches.json file does not exist.\- \\nAccording to the configuration, it should be in\- \\n "- <> toS fname- <> "\- \\n\- \\nFor more information about binary-caches.json, see\n\- \\nhttps://docs.hercules-ci.com/hercules-ci/reference/binary-caches-json/"+ liftIO $+ throwIO $+ FatalError $+ "The binary-caches.json file does not exist.\+ \\nAccording to the configuration, it should be in\+ \\n "+ <> toS fname+ <> "\+ \\n\+ \\nFor more information about binary-caches.json, see\n\+ \\nhttps://docs.hercules-ci.com/hercules-ci/reference/binary-caches-json/" ) bcs <- escalateAs (FatalError . toS) $ eitherDecode bytes validate (toS fname) bcs
+ hercules-ci-agent/Hercules/Agent/Effect.hs view
@@ -0,0 +1,91 @@+module Hercules.Agent.Effect where++import Data.IORef+import qualified Hercules.API.Agent.Effect.EffectTask as EffectTask+import Hercules.API.TaskStatus (TaskStatus)+import qualified Hercules.API.TaskStatus as TaskStatus+import qualified Hercules.Agent.Config as Config+import Hercules.Agent.Env hiding (config)+import qualified Hercules.Agent.Env as Env+import Hercules.Agent.Files+import Hercules.Agent.Log+import qualified Hercules.Agent.Nix as Nix+import Hercules.Agent.Sensitive (Sensitive (Sensitive))+import qualified Hercules.Agent.ServiceInfo as ServiceInfo+import Hercules.Agent.WorkerProcess+import qualified Hercules.Agent.WorkerProcess as WorkerProcess+import qualified Hercules.Agent.WorkerProtocol.Command as Command+import qualified Hercules.Agent.WorkerProtocol.Command.Effect as Command.Effect+import qualified Hercules.Agent.WorkerProtocol.Event as Event+import qualified Hercules.Agent.WorkerProtocol.LogSettings as LogSettings+import qualified Network.URI+import Protolude+import System.FilePath ((</>))+import qualified System.Posix.Signals as PS+import System.Process++performEffect :: EffectTask.EffectTask -> App TaskStatus+performEffect effectTask = withWorkDir "effect" $ \workDir -> do+ workerExe <- getWorkerExe+ commandChan <- liftIO newChan+ extraNixOptions <- Nix.askExtraOptions+ workerEnv <- liftIO $ WorkerProcess.prepareEnv (WorkerProcess.WorkerEnvSettings {nixPath = mempty})+ effectResult <- liftIO $ newIORef Nothing+ let opts = [show extraNixOptions]+ procSpec =+ (System.Process.proc workerExe opts)+ { env = Just workerEnv,+ close_fds = True,+ cwd = Just workDir+ }+ writeEvent :: Event.Event -> App ()+ writeEvent event = case event of+ Event.EffectResult e -> do+ liftIO $ writeIORef effectResult (Just e)+ Event.Exception e -> do+ panic e+ _ -> pass+ config <- asks Env.config+ let materialize = not (Config.nixUserIsTrusted config)+ baseURL <- asks (ServiceInfo.bulkSocketBaseURL . Env.serviceInfo)+ liftIO $+ writeChan commandChan $+ Just $+ Command.Effect $+ Command.Effect.Effect+ { drvPath = EffectTask.derivationPath effectTask,+ inputDerivationOutputPaths = encodeUtf8 <$> EffectTask.inputDerivationOutputPaths effectTask,+ logSettings =+ LogSettings.LogSettings+ { token = Sensitive $ EffectTask.logToken effectTask,+ path = "/api/v1/logs/build/socket",+ baseURL = toS $ Network.URI.uriToString identity baseURL ""+ },+ materializeDerivation = materialize,+ secretsPath = toS $ Config.staticSecretsDirectory config </> "secrets.json",+ token = Sensitive (EffectTask.token effectTask),+ apiBaseURL = Config.herculesApiBaseURL config+ }+ exitCode <- runWorker procSpec (stderrLineHandler "Effect worker") commandChan writeEvent+ logLocM DebugS $ "Worker exit: " <> logStr (show exitCode :: Text)+ let showSig n | n == PS.sigABRT = " (Aborted)"+ showSig n | n == PS.sigBUS = " (Bus)"+ showSig n | n == PS.sigCHLD = " (Child)"+ showSig n | n == PS.sigFPE = " (Floating point exception)"+ showSig n | n == PS.sigHUP = " (Hangup)"+ showSig n | n == PS.sigILL = " (Illegal instruction)"+ showSig n | n == PS.sigINT = " (Interrupted)"+ showSig n | n == PS.sigKILL = " (Killed)"+ showSig n | n == PS.sigPIPE = " (Broken pipe)"+ showSig n | n == PS.sigQUIT = " (Quit)"+ showSig n | n == PS.sigSEGV = " (Segmentation fault)"+ showSig n | n == PS.sigTERM = " (Terminated)"+ showSig _ = ""+ case exitCode of+ ExitSuccess -> pass+ ExitFailure n -> panic $ "Effect worker failed with exit code " <> show n <> showSig (negate $ fromIntegral n)+ liftIO (readIORef effectResult) >>= \case+ Nothing -> pure $ TaskStatus.Exceptional "Effect worker terminated without reporting status"+ Just 0 -> pure $ TaskStatus.Successful ()+ Just n | n > 0 -> pure $ TaskStatus.Terminated ()+ Just n -> pure $ TaskStatus.Exceptional $ "Effect process exited with status code " <> show n <> showSig (negate $ fromIntegral n)
hercules-ci-agent/Hercules/Agent/Env.hs view
@@ -26,33 +26,32 @@ import qualified Network.HTTP.Client import Protolude import qualified Servant.Auth.Client-import qualified Servant.Client+import qualified Servant.Client.Streaming -data Env- = Env- { manager :: Network.HTTP.Client.Manager,- config :: FinalConfig,- herculesBaseUrl :: Servant.Client.BaseUrl,- herculesClientEnv :: Servant.Client.ClientEnv,- serviceInfo :: ServiceInfo.Env,- -- TODO: The implicit limitation here is that we can- -- only have one token at a time. I wouldn't be surprised if this becomes- -- problematic at some point. Perhaps we should switch to a polymorphic- -- reader monad like RIO when we hit that limitation.- currentToken :: Servant.Auth.Client.Token,- binaryCaches :: Config.BinaryCaches.BinaryCaches,- cachixEnv :: Cachix.Env,- nixEnv :: Nix.Env,- socket :: AgentSocket,- -- katip- kNamespace :: K.Namespace,- kContext :: K.LogContexts,- kLogEnv :: K.LogEnv- }+data Env = Env+ { manager :: Network.HTTP.Client.Manager,+ config :: FinalConfig,+ herculesBaseUrl :: Servant.Client.Streaming.BaseUrl,+ herculesClientEnv :: Servant.Client.Streaming.ClientEnv,+ serviceInfo :: ServiceInfo.Env,+ -- TODO: The implicit limitation here is that we can+ -- only have one token at a time. I wouldn't be surprised if this becomes+ -- problematic at some point. Perhaps we should switch to a polymorphic+ -- reader monad like RIO when we hit that limitation.+ currentToken :: Servant.Auth.Client.Token,+ binaryCaches :: Config.BinaryCaches.BinaryCaches,+ cachixEnv :: Cachix.Env,+ nixEnv :: Nix.Env,+ socket :: AgentSocket,+ -- katip+ kNamespace :: K.Namespace,+ kContext :: K.LogContexts,+ kLogEnv :: K.LogEnv+ } activePushCaches :: App [Text] activePushCaches = do- bc <- asks (binaryCaches)+ bc <- asks binaryCaches pure $ M.keys ( void (Config.BinaryCaches.cachixCaches bc)@@ -71,25 +70,24 @@ runApp env (App m) = runReaderT m env runHerculesClient ::- (Servant.Auth.Client.Token -> Servant.Client.ClientM a) ->+ NFData a =>+ (Servant.Auth.Client.Token -> Servant.Client.Streaming.ClientM a) -> App a runHerculesClient f = do tok <- asks currentToken runHerculesClient' (f tok) -runHerculesClient' :: Servant.Client.ClientM a -> App a+runHerculesClient' :: NFData a => Servant.Client.Streaming.ClientM a -> App a runHerculesClient' m = do clientEnv <- asks herculesClientEnv- escalate =<< liftIO (Servant.Client.runClientM m clientEnv)+ escalate =<< liftIO (Servant.Client.Streaming.runClientM m clientEnv) instance K.Katip App where- getLogEnv = asks kLogEnv localLogEnv f (App m) = App (local (\s -> s {kLogEnv = f (kLogEnv s)}) m) instance K.KatipContext App where- getKatipContext = asks kContext localKatipContext f (App m) = App (local (\s -> s {kContext = f (kContext s)}) m)
hercules-ci-agent/Hercules/Agent/EnvironmentInfo.hs view
@@ -1,19 +1,18 @@ module Hercules.Agent.EnvironmentInfo where import Control.Lens- ( (^..),+ ( to,+ (^..), (^?),- to, ) import qualified Data.Aeson as Aeson import Data.Aeson.Lens- ( _Array,+ ( key,+ _Array, _Number, _String,- key, ) import qualified Data.ByteString.Lazy as LBS-import Data.Char (isSpace) import qualified Data.Text as T import qualified Hercules.API.Agent.LifeCycle.AgentInfo as AgentInfo import Hercules.Agent.CabalInfo as CabalInfo@@ -32,29 +31,30 @@ cachixPushCaches <- Cachix.Info.activePushCaches pushCaches <- Env.activePushCaches concurrentTasks <- asks (Config.concurrentTasks . Env.config)- let s = AgentInfo.AgentInfo- { hostname = toS hostname,- agentVersion = CabalInfo.herculesAgentVersion, -- TODO: Add git revision- nixVersion = nixExeVersion nix,- platforms = nixPlatforms nix,- cachixPushCaches = cachixPushCaches,- pushCaches = pushCaches,- systemFeatures = nixSystemFeatures nix,- substituters = nixSubstituters nix, -- TODO: Add cachix substituters- concurrentTasks = fromIntegral concurrentTasks- }+ let s =+ AgentInfo.AgentInfo+ { hostname = toS hostname,+ agentVersion = CabalInfo.herculesAgentVersion, -- TODO: Add git revision+ nixVersion = nixExeVersion nix,+ platforms = nixPlatforms nix,+ cachixPushCaches = cachixPushCaches,+ pushCaches = pushCaches,+ systemFeatures = nixSystemFeatures nix,+ substituters = nixSubstituters nix, -- TODO: Add cachix substituters+ concurrentTasks = fromIntegral concurrentTasks+ } logLocM DebugS $ "Determined environment info: " <> logStr (show s :: Text) pure s -data NixInfo- = NixInfo- { nixExeVersion :: Text,- nixPlatforms :: [Text],- nixSystemFeatures :: [Text],- nixSubstituters :: [Text],- nixTrustedPublicKeys :: [Text],- nixNarinfoCacheNegativeTTL :: Maybe Integer- }+data NixInfo = NixInfo+ { nixExeVersion :: Text,+ nixPlatforms :: [Text],+ nixSystemFeatures :: [Text],+ nixSubstituters :: [Text],+ nixTrustedPublicKeys :: [Text],+ nixNarinfoCacheNegativeTTL :: Maybe Integer,+ nixNetrcFile :: Maybe Text+ } getNixInfo :: IO NixInfo getNixInfo = do@@ -62,50 +62,56 @@ version <- Process.readProcess "nix" ["--version"] stdinEmpty rawJson <- Process.readProcess "nix" ["show-config", "--json"] stdinEmpty cfg <-- case Aeson.eitherDecode (LBS.fromStrict $ encodeUtf8 $ toS $ rawJson) of+ case Aeson.eitherDecode (LBS.fromStrict $ encodeUtf8 $ toS rawJson) of Left e -> panic $ "Could not parse nix show-config --json: " <> show e Right r -> pure r- pure NixInfo- { nixExeVersion = T.dropAround isSpace (toS version),- nixPlatforms =- ((cfg :: Aeson.Value) ^.. key "system" . key "value" . _String)- <> ( cfg- ^.. key "extra-platforms"- . key "value"- . _Array- . traverse- . _String- ),- nixSystemFeatures =- cfg- ^.. key "system-features"- . key "value"- . _Array- . traverse- . _String,- nixSubstituters =- cfg- ^.. key "substituters"- . key "value"- . _Array- . traverse- . _String- . to cleanUrl,- nixTrustedPublicKeys =- cfg- ^.. key "trusted-public-keys"- . key "value"- . _Array- . traverse- . _String- . to cleanUrl,- nixNarinfoCacheNegativeTTL =- cfg- ^? key "narinfo-cache-negative-ttl"- . key "value"- . _Number- . to floor- }+ pure+ NixInfo+ { nixExeVersion = T.dropAround isSpace (toS version),+ nixPlatforms =+ ((cfg :: Aeson.Value) ^.. key "system" . key "value" . _String)+ <> ( cfg+ ^.. key "extra-platforms"+ . key "value"+ . _Array+ . traverse+ . _String+ ),+ nixSystemFeatures =+ cfg+ ^.. key "system-features"+ . key "value"+ . _Array+ . traverse+ . _String,+ nixSubstituters =+ cfg+ ^.. key "substituters"+ . key "value"+ . _Array+ . traverse+ . _String+ . to cleanUrl,+ nixTrustedPublicKeys =+ cfg+ ^.. key "trusted-public-keys"+ . key "value"+ . _Array+ . traverse+ . _String+ . to cleanUrl,+ nixNarinfoCacheNegativeTTL =+ cfg+ ^? key "narinfo-cache-negative-ttl"+ . key "value"+ . _Number+ . to floor,+ nixNetrcFile =+ cfg+ ^? key "netrc-file"+ . key "value"+ . _String+ } cleanUrl :: Text -> Text cleanUrl t | "@" `T.isInfixOf` t = "<URI censored; might contain secret>"
hercules-ci-agent/Hercules/Agent/Evaluate.hs view
@@ -4,14 +4,16 @@ module Hercules.Agent.Evaluate ( performEvaluation,+ findNixFile, ) where -import CNix (withStore) import Conduit import qualified Control.Concurrent.Async.Lifted as Async.Lifted import Control.Concurrent.Chan.Lifted-import Control.Monad.IO.Unlift+import qualified Data.Aeson as A+import qualified Data.ByteString.Lazy as BL+import Data.Char (isAsciiLower, isAsciiUpper) import Data.Conduit.Process (sourceProcessWithStreams) import Data.IORef ( atomicModifyIORef,@@ -39,19 +41,21 @@ import Hercules.API.Servant (noContent) import qualified Hercules.Agent.Cache as Agent.Cache import qualified Hercules.Agent.Client-import qualified Hercules.Agent.Config as Config import Hercules.Agent.Env import qualified Hercules.Agent.Env as Env import qualified Hercules.Agent.Evaluate.TraversalQueue as TraversalQueue+import Hercules.Agent.Files import Hercules.Agent.Log import qualified Hercules.Agent.Nix as Nix import Hercules.Agent.Nix.RetrieveDerivationInfo ( retrieveDerivationInfo, )+import Hercules.Agent.NixFile (findNixFile) import Hercules.Agent.NixPath ( renderSubPath, ) import Hercules.Agent.Producer+import Hercules.Agent.Sensitive (Sensitive (Sensitive)) import qualified Hercules.Agent.ServiceInfo as ServiceInfo import Hercules.Agent.WorkerProcess () import qualified Hercules.Agent.WorkerProcess as WorkerProcess@@ -62,6 +66,7 @@ import qualified Hercules.Agent.WorkerProtocol.Event.Attribute as WorkerAttribute import qualified Hercules.Agent.WorkerProtocol.Event.AttributeError as WorkerAttributeError import qualified Hercules.Agent.WorkerProtocol.LogSettings as LogSettings+import Hercules.CNix (withStore) import Hercules.Error (defaultRetry, quickRetry) import qualified Network.HTTP.Client.Conduit as HTTP.Conduit import qualified Network.HTTP.Simple as HTTP.Simple@@ -70,7 +75,6 @@ import qualified Servant.Client import qualified System.Directory as Dir import System.FilePath-import System.IO.Temp (withTempDirectory) import System.Process eventLimit :: Int@@ -90,19 +94,34 @@ EvaluateTask.EvaluateTask -> (Syncing EvaluateEvent.EvaluateEvent -> App ()) -> App ()-produceEvaluationTaskEvents task writeToBatch = withWorkDir $ \tmpdir -> do+produceEvaluationTaskEvents task writeToBatch = withWorkDir "eval" $ \tmpdir -> do logLocM DebugS "Retrieving evaluation task" let emitSingle = writeToBatch . Syncable sync = syncer writeToBatch- projectDir <-- fetchSource- (tmpdir </> "primary")- (EvaluateTask.primaryInput task)- withNamedContext "projectDir" projectDir $- logLocM DebugS "Determined projectDir" inputLocations <-- flip M.traverseWithKey (EvaluateTask.otherInputs task) $- \k src -> fetchSource (tmpdir </> ("arg-" <> toS k)) src+ EvaluateTask.otherInputs task+ & M.traverseWithKey \k src -> do+ let fetchName = tmpdir </> ("fetch-" <> toS k)+ argName = tmpdir </> ("arg-" <> toS k)+ metaName = do+ meta <- EvaluateTask.inputMetadata task & M.lookup k+ nameValue <- meta & M.lookup "name"+ name <- case A.fromJSON nameValue of+ A.Success a -> pure a+ _ -> Nothing+ guard (isValidName name)+ pure name+ destName+ | Just sourceName <- metaName = argName </> sourceName+ | otherwise = argName+ fetched <- fetchSource fetchName src+ liftIO do+ Dir.createDirectoryIfMissing True (takeDirectory destName)+ renamePathTryHarder fetched destName+ pure destName+ projectDir <- case M.lookup "src" inputLocations of+ Nothing -> panic "No primary source provided"+ Just x -> pure x nixPath <- EvaluateTask.nixPath task & ( traverse@@ -111,10 +130,10 @@ $ \identifier -> case M.lookup identifier inputLocations of Just x -> pure x Nothing ->- throwIO- $ FatalError- $ "Nix path references undefined input "- <> identifier+ throwIO $+ FatalError $+ "Nix path references undefined input "+ <> identifier ) autoArguments' <- EvaluateTask.autoArguments task@@ -122,20 +141,30 @@ ( \identifier -> case M.lookup identifier inputLocations of Just x | "/" `isPrefixOf` x -> pure x Just x ->- throwIO- $ FatalError- $ "input "- <> identifier- <> " was not resolved to an absolute path: "- <> toS x+ throwIO $+ FatalError $+ "input "+ <> identifier+ <> " was not resolved to an absolute path: "+ <> toS x Nothing ->- throwIO- $ FatalError- $ "auto argument references undefined input "- <> identifier+ throwIO $+ FatalError $+ "auto argument references undefined input "+ <> identifier )- let autoArguments = autoArguments'- <&> \sp -> Eval.ExprArg $ encodeUtf8 $ renderSubPath $ toS <$> sp+ let autoArguments =+ autoArguments'+ & M.mapWithKey \k sp ->+ let argPath = encodeUtf8 $ renderSubPath $ toS <$> sp+ in case do+ inputId <- EvaluateTask.autoArguments task & M.lookup k+ EvaluateTask.inputMetadata task & M.lookup (EvaluateTask.path inputId) of+ Nothing -> Eval.ExprArg argPath+ Just attrs ->+ Eval.ExprArg $+ -- TODO pass directly to avoid having to escape (or just escape properly)+ "builtins.fromJSON ''" <> BL.toStrict (A.encode attrs) <> "'' // { outPath = " <> argPath <> "; }" msgCounter <- liftIO $ newIORef 0 let fixIndex :: MonadIO m =>@@ -154,14 +183,15 @@ then do truncMsg <- fixIndex $- EvaluateEvent.Message Message.Message- { index = -1,- typ = Message.Error,- message =- "Evaluation limit reached. Does your nix expression produce infinite attributes? Please make sure that your project is finite. If it really does require more than "- <> show eventLimit- <> " attributes or messages, please contact info@hercules-ci.com."- }+ EvaluateEvent.Message+ Message.Message+ { index = -1,+ typ = Message.Error,+ message =+ "Evaluation limit reached. Does your nix expression produce infinite attributes? Please make sure that your project is finite. If it really does require more than "+ <> show eventLimit+ <> " attributes or messages, please contact info@hercules-ci.com."+ } emitSingle truncMsg panic "Evaluation limit reached." else emitSingle =<< fixIndex update@@ -170,11 +200,12 @@ liftIO (findNixFile projectDir) >>= \case Left e -> emit $- EvaluateEvent.Message Message.Message- { Message.index = -1, -- will be set by emit- Message.typ = Message.Error,- Message.message = e- }+ EvaluateEvent.Message+ Message.Message+ { Message.index = -1, -- will be set by emit+ Message.typ = Message.Error,+ Message.message = e+ } Right file -> TraversalQueue.with $ \derivationQueue -> let doIt = do Async.Lifted.concurrently_ evaluation emitDrvs@@ -223,6 +254,16 @@ emit $ EvaluateEvent.DerivationInfo drvInfo in doIt +isValidName :: FilePath -> Bool+isValidName "" = False+isValidName cs@(c0 : _) = all isValidNameChar cs && c0 /= '.'+ where+ isValidNameChar c =+ isAsciiUpper c+ || isAsciiLower c+ || isDigit c+ || c `elem` ("+-._?=" :: [Char])+ runEvalProcess :: FilePath -> FilePath ->@@ -239,17 +280,19 @@ runEvalProcess projectDir file autoArguments nixPath emit uploadDerivationInfos flush logToken = do extraOpts <- Nix.askExtraOptions baseURL <- asks (ServiceInfo.bulkSocketBaseURL . Env.serviceInfo)- let eval = Eval.Eval- { Eval.cwd = projectDir,- Eval.file = toS file,- Eval.autoArguments = autoArguments,- Eval.extraNixOptions = extraOpts,- Eval.logSettings = LogSettings.LogSettings- { token = LogSettings.Sensitive logToken,- path = "/api/v1/logs/build/socket",- baseURL = toS $ Network.URI.uriToString identity baseURL ""- }- }+ let eval =+ Eval.Eval+ { Eval.cwd = projectDir,+ Eval.file = toS file,+ Eval.autoArguments = autoArguments,+ Eval.extraNixOptions = extraOpts,+ Eval.logSettings =+ LogSettings.LogSettings+ { token = Sensitive logToken,+ path = "/api/v1/logs/build/socket",+ baseURL = toS $ Network.URI.uriToString identity baseURL ""+ }+ } buildRequiredIndex <- liftIO $ newIORef (0 :: Int) commandChan <- newChan writeChan commandChan $ Just $ Command.Eval eval@@ -261,39 +304,51 @@ workerEventsP ( \case Event.Attribute a -> do- emit $ EvaluateEvent.Attribute $ AttributeEvent.AttributeEvent- { AttributeEvent.expressionPath = decode <$> WorkerAttribute.path a,- AttributeEvent.derivationPath = decode $ WorkerAttribute.drv a- }+ emit $+ EvaluateEvent.Attribute $+ AttributeEvent.AttributeEvent+ { AttributeEvent.expressionPath = decode <$> WorkerAttribute.path a,+ AttributeEvent.derivationPath = decode $ WorkerAttribute.drv a,+ AttributeEvent.typ = case WorkerAttribute.typ a of+ WorkerAttribute.Regular -> AttributeEvent.Regular+ WorkerAttribute.MustFail -> AttributeEvent.MustFail+ WorkerAttribute.MayFail -> AttributeEvent.MayFail+ WorkerAttribute.Effect -> AttributeEvent.Effect+ WorkerAttribute.DependenciesOnly -> AttributeEvent.DependenciesOnly+ } continue Event.AttributeError e -> do- emit $ EvaluateEvent.AttributeError $ AttributeErrorEvent.AttributeErrorEvent- { AttributeErrorEvent.expressionPath = decode <$> WorkerAttributeError.path e,- AttributeErrorEvent.errorMessage = WorkerAttributeError.message e,- AttributeErrorEvent.errorType = WorkerAttributeError.errorType e,- AttributeErrorEvent.errorDerivation = WorkerAttributeError.errorDerivation e- }+ emit $+ EvaluateEvent.AttributeError $+ AttributeErrorEvent.AttributeErrorEvent+ { AttributeErrorEvent.expressionPath = decode <$> WorkerAttributeError.path e,+ AttributeErrorEvent.errorMessage = WorkerAttributeError.message e,+ AttributeErrorEvent.errorType = WorkerAttributeError.errorType e,+ AttributeErrorEvent.errorDerivation = WorkerAttributeError.errorDerivation e+ } continue Event.EvaluationDone -> writeChan commandChan Nothing Event.Error e -> do emit $- EvaluateEvent.Message Message.Message- { Message.index = -1, -- will be set by emit- Message.typ = Message.Error,- Message.message = e- }+ EvaluateEvent.Message+ Message.Message+ { Message.index = -1, -- will be set by emit+ Message.typ = Message.Error,+ Message.message = e+ } continue Event.Build drv outputName notAttempt -> do status <- withNamedContext "derivation" drv $ do currentIndex <- liftIO $ atomicModifyIORef buildRequiredIndex (\i -> (i + 1, i)) emit $- EvaluateEvent.BuildRequired BuildRequired.BuildRequired- { BuildRequired.derivationPath = drv,- BuildRequired.index = currentIndex,- BuildRequired.outputName = outputName- }+ EvaluateEvent.BuildRequired+ BuildRequired.BuildRequired+ { BuildRequired.derivationPath = drv,+ BuildRequired.index = currentIndex,+ BuildRequired.outputName = outputName+ } let pushDerivations = do caches <- activePushCaches forM_ caches $ \cache -> do@@ -302,19 +357,22 @@ Async.Lifted.concurrently_ (uploadDerivationInfos drv) pushDerivations- emit $ EvaluateEvent.BuildRequest BuildRequest.BuildRequest- { derivationPath = drv,- forceRebuild = isJust notAttempt- }+ emit $+ EvaluateEvent.BuildRequest+ BuildRequest.BuildRequest+ { derivationPath = drv,+ forceRebuild = isJust notAttempt+ } flush status <- drvPoller notAttempt drv logLocM DebugS $ "Got derivation status " <> logStr (show status :: Text) return status writeChan commandChan $ Just $ Command.BuildResult $ uncurry (BuildResult.BuildResult drv) status continue+ Event.Exception e -> panic e -- Unused during eval Event.BuildResult {} -> pass- Event.Exception e -> panic e+ Event.EffectResult {} -> pass ) ( \case ExitSuccess -> logLocM DebugS "Clean worker exit"@@ -335,22 +393,22 @@ -- NiceToHave: replace renderNixPath by something structured like -I -- to support = and : in paths workerEnv <- liftIO $ WorkerProcess.prepareEnv (WorkerProcess.WorkerEnvSettings {nixPath = nixPath})- wps <-- pure- (System.Process.proc workerExe opts)- { env = Just workerEnv,- close_fds = True, -- Disable on Windows?- cwd = Just (Eval.cwd eval)- }+ let wps =+ (System.Process.proc workerExe opts)+ { env = Just workerEnv,+ close_fds = True, -- Disable on Windows?+ cwd = Just (Eval.cwd eval)+ } WorkerProcess.runWorker wps (stderrLineHandler "evaluator") commandChan writeEvent drvPoller :: Maybe UUID -> Text -> App (UUID, BuildResult.BuildStatus) drvPoller notAttempt drvPath = do resp <-- defaultRetry $ runHerculesClient $- getDerivationStatus2- Hercules.Agent.Client.evalClient- drvPath+ defaultRetry $+ runHerculesClient $+ getDerivationStatus2+ Hercules.Agent.Client.evalClient+ drvPath let oneSecond = 1000 * 1000 again = do liftIO $ threadDelay oneSecond@@ -377,18 +435,18 @@ -- Fewer retries in order to speed up the tests. quickRetry $ do (x, _, _) <-- liftIO- $ (`runReaderT` Servant.Client.manager clientEnv)- $ HTTP.Conduit.withResponse request- $ \response -> do- let tarball = HTTP.Conduit.responseBody response- procSpec =- (System.Process.proc "tar" ["-xz"]) {cwd = Just targetDir}- sourceProcessWithStreams- procSpec- tarball- Conduit.stderrC- Conduit.stderrC+ liftIO $+ (`runReaderT` Servant.Client.manager clientEnv) $+ HTTP.Conduit.withResponse request $+ \response -> do+ let tarball = HTTP.Conduit.responseBody response+ procSpec =+ (System.Process.proc "tar" ["-xz"]) {cwd = Just targetDir}+ sourceProcessWithStreams+ procSpec+ tarball+ Conduit.stderrC+ Conduit.stderrC case x of ExitSuccess -> pass ExitFailure {} -> throwIO $ SubprocessFailure "Extracting tarball"@@ -402,64 +460,19 @@ findTarballDir fp = do nodes <- Dir.listDirectory fp case nodes of- [x] -> Dir.doesDirectoryExist (fp </> x) >>= \case- True -> pure $ fp </> x- False -> pure fp+ [x] ->+ Dir.doesDirectoryExist (fp </> x) >>= \case+ True -> pure $ fp </> x+ False -> pure fp _ -> pure fp -type Ambiguity = [FilePath]--searchPath :: [Ambiguity]-searchPath = [["nix/ci.nix", "ci.nix"], ["default.nix"]]--findNixFile :: FilePath -> IO (Either Text FilePath)-findNixFile projectDir = do- searchResult <-- for searchPath $ traverse $ \relPath ->- let path = projectDir </> relPath- in Dir.doesFileExist path >>= \case- True -> pure $ Just (relPath, path)- False -> pure Nothing- case filter (not . null) $ map catMaybes searchResult of- [(_relPath, unambiguous)] : _ -> pure (pure unambiguous)- ambiguous : _ ->- pure- $ Left- $ "Don't know what to do, expecting only one of "- <> englishConjunction "or" (map fst ambiguous)- [] ->- pure- $ Left- $ "Please provide a Nix expression to build. Could not find any of "- <> englishConjunction "or" (concat searchPath)- <> " in your source"--englishConjunction :: Show a => Text -> [a] -> Text-englishConjunction _ [] = "none"-englishConjunction _ [a] = show a-englishConjunction connective [a1, a2] =- show a1 <> " " <> connective <> " " <> show a2-englishConjunction connective (a : as) =- show a <> ", " <> englishConjunction connective as- postBatch :: EvaluateTask.EvaluateTask -> [EvaluateEvent.EvaluateEvent] -> App () postBatch task events =- noContent $ defaultRetry $- runHerculesClient- ( tasksUpdateEvaluation- Hercules.Agent.Client.evalClient- (EvaluateTask.id task)- events- )---- TODO: configurable temp directory-withWorkDir :: (FilePath -> App b) -> App b-withWorkDir f = do- UnliftIO {unliftIO = unlift} <- askUnliftIO- workDir <- asks (Config.workDirectory . config)- liftIO $ withTempDirectory workDir "eval" $ unlift . f--readFileMaybe :: MonadIO m => FilePath -> m (Maybe Text)-readFileMaybe fp = liftIO do- exists <- Dir.doesFileExist fp- guard exists & traverse \_ -> readFile fp+ noContent $+ defaultRetry $+ runHerculesClient+ ( tasksUpdateEvaluation+ Hercules.Agent.Client.evalClient+ (EvaluateTask.id task)+ events+ )
hercules-ci-agent/Hercules/Agent/Evaluate/TraversalQueue.hs view
@@ -29,13 +29,12 @@ writeChan, ) -data Queue a- = Queue- { chan :: Chan (Maybe a),- visitedSet :: IORef (Set a),- -- | Increased on enqueue, decreased when processed- size :: TVar Int- }+data Queue a = Queue+ { chan :: Chan (Maybe a),+ visitedSet :: IORef (Set a),+ -- | Increased on enqueue, decreased when processed+ size :: TVar Int+ } with :: MonadBaseControl IO m => (Queue a -> m ()) -> m () with = Control.Exception.Lifted.bracket new close@@ -54,9 +53,10 @@ writeChan (chan env) (Just msg) waitUntilDone :: MonadBase IO m => Queue a -> m ()-waitUntilDone env = liftBase $ atomically $ do- n <- readTVar (size env)- check (n == 0)+waitUntilDone env = liftBase $+ atomically $ do+ n <- readTVar (size env)+ check (n == 0) readJust_ :: (MonadBase IO m, MonadIO m) =>
+ hercules-ci-agent/Hercules/Agent/Files.hs view
@@ -0,0 +1,41 @@+{-# LANGUAGE BlockArguments #-}++module Hercules.Agent.Files where++import Control.Monad.IO.Unlift+import qualified Hercules.Agent.Config as Config+import Hercules.Agent.Env+import Protolude+import System.Directory+import System.FilePath+import System.IO.Error+import System.IO.Temp++withWorkDir :: Text -> (FilePath -> App b) -> App b+withWorkDir hint f = do+ UnliftIO {unliftIO = unlift} <- askUnliftIO+ workDir <- asks (Config.workDirectory . config)+ liftIO $ withTempDirectory workDir (toS hint) $ unlift . f++readFileMaybe :: MonadIO m => FilePath -> m (Maybe Text)+readFileMaybe fp = liftIO do+ exists <- doesFileExist fp+ guard exists & traverse \_ -> readFile fp++renamePathTryHarder :: FilePath -> FilePath -> IO ()+renamePathTryHarder source dest = do+ catchJust+ ( \case+ e | isPermissionError e -> Just e+ _ -> Nothing+ )+ doIt+ (const tryHarder)+ where+ doIt = renamePath source dest+ modifyPermissions f p = getPermissions p >>= setPermissions p . f+ tryHarder = do+ modifyPermissions (setOwnerWritable True) (takeDirectory source)+ modifyPermissions (setOwnerWritable True) source+ modifyPermissions (setOwnerWritable True) (takeDirectory dest)+ doIt
hercules-ci-agent/Hercules/Agent/Init.hs view
@@ -1,16 +1,16 @@ module Hercules.Agent.Init where -import qualified CNix import qualified Hercules.Agent.Cachix.Init import qualified Hercules.Agent.Compat as Compat import qualified Hercules.Agent.Config as Config import qualified Hercules.Agent.Config.BinaryCaches as BC-import qualified Hercules.Agent.Env as Env import Hercules.Agent.Env (Env (Env))+import qualified Hercules.Agent.Env as Env import qualified Hercules.Agent.Nix.Init import qualified Hercules.Agent.SecureDirectory as SecureDirectory import qualified Hercules.Agent.ServiceInfo as ServiceInfo import qualified Hercules.Agent.Token as Token+import qualified Hercules.CNix import qualified Katip as K import qualified Network.HTTP.Client.TLS import Protolude@@ -34,21 +34,22 @@ cachix <- withLogging $ Hercules.Agent.Cachix.Init.newEnv config (BC.cachixCaches bcs) nix <- Hercules.Agent.Nix.Init.newEnv serviceInfo <- ServiceInfo.newEnv clientEnv- pure Env- { manager = manager,- config = config,- herculesBaseUrl = baseUrl,- herculesClientEnv = clientEnv,- serviceInfo = serviceInfo,- currentToken = Servant.Auth.Client.Token $ encodeUtf8 token,- binaryCaches = bcs,- cachixEnv = cachix,- socket = panic "Socket not defined yet.", -- Hmm, needs different monad?- kNamespace = emptyNamespace,- kContext = mempty,- kLogEnv = logEnv,- nixEnv = nix- }+ pure+ Env+ { manager = manager,+ config = config,+ herculesBaseUrl = baseUrl,+ herculesClientEnv = clientEnv,+ serviceInfo = serviceInfo,+ currentToken = Servant.Auth.Client.Token $ encodeUtf8 token,+ binaryCaches = bcs,+ cachixEnv = cachix,+ socket = panic "Socket not defined yet.", -- Hmm, needs different monad?+ kNamespace = emptyNamespace,+ kContext = mempty,+ kLogEnv = logEnv,+ nixEnv = nix+ } setupLogging :: Config.FinalConfig -> (K.LogEnv -> IO ()) -> IO () setupLogging cfg f = do@@ -63,5 +64,5 @@ initCNix :: IO () initCNix = do- CNix.init- CNix.setTalkative+ Hercules.CNix.init+ Hercules.CNix.setTalkative
hercules-ci-agent/Hercules/Agent/Log.hs view
@@ -38,7 +38,7 @@ -- "This is the lowest level function [...] useful when implementing centralised logging services." Katip.Core.logKatipItem (Katip.Core.SimpleLogPayload . M.toList . fmap (Katip.Core.AnyLogPayload :: A.Value -> Katip.Core.AnyLogPayload) <$> item) stderrLineHandler processRole pid ln =- withNamedContext "worker" (pid :: Int)- $ logLocM InfoS- $ logStr- $ processRole <> ": " <> decodeUtf8With lenientDecode ln+ withNamedContext "worker" (pid :: Int) $+ logLocM InfoS $+ logStr $+ processRole <> ": " <> decodeUtf8With lenientDecode ln
hercules-ci-agent/Hercules/Agent/Nix.hs view
@@ -1,8 +1,13 @@ module Hercules.Agent.Nix where +import Control.Monad.Trans.Maybe+import qualified Data.ByteString as BS+import qualified Data.Text as T import Hercules.Agent.Env as Agent.Env+import Hercules.Agent.EnvironmentInfo (getNixInfo, nixNetrcFile) import Hercules.Agent.Nix.Env as Nix.Env import Protolude+import System.Directory (doesFileExist) import System.Process (readProcess) import qualified System.Process @@ -42,3 +47,17 @@ nixProc exe opts args = do extraOpts <- getExtraOptionArguments pure $ System.Process.proc (toS exe) (map toS (opts <> extraOpts <> ["--"] <> args))++getNetrcLines :: App [Text]+getNetrcLines = liftIO $ do+ info <- getNixInfo+ let fm = toS <$> nixNetrcFile info+ fmap fold <$> runMaybeT $ do+ f <- MaybeT $ pure fm+ exs <- lift $ doesFileExist f+ guard exs+ bs <- liftIO $ BS.readFile f+ pure $+ bs+ & decodeUtf8+ & T.lines
hercules-ci-agent/Hercules/Agent/Nix/Env.hs view
@@ -2,7 +2,6 @@ import Protolude -data Env- = Env- { extraOptions :: [(Text, Text)]- }+data Env = Env+ { extraOptions :: [(Text, Text)]+ }
hercules-ci-agent/Hercules/Agent/Nix/Init.hs view
@@ -28,6 +28,7 @@ throwIO $ FatalError "Please configure your system's Nix with: narinfo-cache-negative-ttl = 0 " -- Might want to take stuff from Config and put it in -- extraOptions here.- pure Env- { extraOptions = []- }+ pure+ Env+ { extraOptions = []+ }
hercules-ci-agent/Hercules/Agent/Nix/RetrieveDerivationInfo.hs view
@@ -1,7 +1,5 @@ module Hercules.Agent.Nix.RetrieveDerivationInfo where -import CNix-import CNix.Internal.Context (Derivation) import qualified Data.Map as M import qualified Data.Text as T import Foreign.ForeignPtr (ForeignPtr)@@ -9,11 +7,12 @@ ( DerivationInfo (DerivationInfo), ) import qualified Hercules.API.Agent.Evaluate.EvaluateEvent.DerivationInfo as DerivationInfo+import Hercules.CNix import Protolude retrieveDerivationInfo :: MonadIO m =>- Ptr (Ref NixStore) ->+ Store -> DerivationInfo.DerivationPathText -> m DerivationInfo retrieveDerivationInfo store drvPath = liftIO $ do@@ -30,22 +29,23 @@ let requiredSystemFeatures = maybe [] splitFeatures $ M.lookup "requiredSystemFeatures" env splitFeatures = filter (not . T.null) . T.split (== ' ') . decode decode = decodeUtf8With lenientDecode- pure $ DerivationInfo- { derivationPath = drvPath,- platform = decodeUtf8With lenientDecode platform,- requiredSystemFeatures = requiredSystemFeatures,- inputDerivations = inputDrvPaths & map (\(i, os) -> (decode i, map decode os)) & M.fromList,- inputSources = map decode sourcePaths,- outputs =- outputs- & map- ( \output ->- ( decode $ derivationOutputName output,- DerivationInfo.OutputInfo- { DerivationInfo.path = decode $ derivationOutputPath output,- DerivationInfo.isFixed = derivationOutputHashAlgo output /= ""- }- )- )- & M.fromList- }+ pure $+ DerivationInfo+ { derivationPath = drvPath,+ platform = decode platform,+ requiredSystemFeatures = requiredSystemFeatures,+ inputDerivations = inputDrvPaths & map (bimap decode (map decode)) & M.fromList,+ inputSources = map decode sourcePaths,+ outputs =+ outputs+ & map+ ( \output ->+ ( decode $ derivationOutputName output,+ DerivationInfo.OutputInfo+ { DerivationInfo.path = decode $ derivationOutputPath output,+ DerivationInfo.isFixed = derivationOutputHashAlgo output /= ""+ }+ )+ )+ & M.fromList+ }
hercules-ci-agent/Hercules/Agent/Options.hs view
@@ -10,11 +10,10 @@ import Options.Applicative import Protolude hiding (option) -data Options- = Options- { configFile :: ConfigPath,- mode :: Mode- }+data Options = Options+ { configFile :: ConfigPath,+ mode :: Mode+ } parseOptions :: Parser Options parseOptions =
hercules-ci-agent/Hercules/Agent/ServiceInfo.hs view
@@ -6,27 +6,28 @@ import Hercules.Error (escalate) import Network.URI import Protolude-import qualified Servant.Client+import qualified Servant.Client.Streaming -data Env- = Env- { agentSocketBaseURL :: URI,- bulkSocketBaseURL :: URI- }+data Env = Env+ { agentSocketBaseURL :: URI,+ bulkSocketBaseURL :: URI+ } -newEnv :: Servant.Client.ClientEnv -> IO Env+newEnv :: Servant.Client.Streaming.ClientEnv -> IO Env newEnv clientEnv = do- serviceInfo <- escalate =<< Servant.Client.runClientM (API.LifeCycle.getServiceInfo Client.lifeCycleClient) clientEnv+ serviceInfo <- escalate =<< Servant.Client.Streaming.runClientM (API.LifeCycle.getServiceInfo Client.lifeCycleClient) clientEnv let parse varName text = case parseURI (toS text) of Nothing -> panic $ varName <> " invalid: " <> ServiceInfo.agentSocketBaseURL serviceInfo- Just uri -> uri <$ do- case uriScheme uri of- "http:" -> pass- "https:" -> pass- x -> panic $ varName <> " has invalid uri scheme" <> toS x+ Just uri ->+ uri <$ do+ case uriScheme uri of+ "http:" -> pass+ "https:" -> pass+ x -> panic $ varName <> " has invalid uri scheme" <> toS x agentSocketBaseURL_ <- parse "agentSocketBaseURL" $ ServiceInfo.agentSocketBaseURL serviceInfo bulkSocketBaseURL_ <- parse "bulkSocketBaseURL" $ ServiceInfo.bulkSocketBaseURL serviceInfo- pure Env- { agentSocketBaseURL = agentSocketBaseURL_,- bulkSocketBaseURL = bulkSocketBaseURL_- }+ pure+ Env+ { agentSocketBaseURL = agentSocketBaseURL_,+ bulkSocketBaseURL = bulkSocketBaseURL_+ }
hercules-ci-agent/Hercules/Agent/Token.hs view
@@ -2,7 +2,7 @@ import Control.Lens ((^?)) import qualified Data.Aeson as Aeson-import Data.Aeson.Lens (_String, key)+import Data.Aeson.Lens (key, _String) import qualified Data.ByteString.Base64.Lazy as B64L import qualified Data.ByteString.Lazy as BL import qualified Data.Text as T@@ -50,26 +50,27 @@ False -> pure Nothing ensureAgentSession :: App Text-ensureAgentSession = readAgentSessionKey >>= \case- Just sessKey -> do- logLocM DebugS "Found agent session key"- let handler e = do- logLocM WarningS $ "Failed to check whether session token matches cluster join token. Using old session. Remove session.key if you need to force a session update. Exception: " <> logStr (displayException e)- pure sessKey- safeLiftedHandle handler $ do- cjt <- getClusterJoinTokenId- logLocM DebugS $ "Found clusterJoinTokenId " <> logStr cjt- scjt <- getSessionClusterJoinTokenId sessKey- logLocM DebugS $ "Found sessionClusterJoinTokenId " <> logStr scjt- if cjt == scjt- then pure sessKey- else do- logLocM DebugS "Getting new session key to match cluster join token..."- updateAgentSession- Nothing -> do- writeAgentSessionKey "x" -- Sanity check- logLocM DebugS "Creating agent session"- updateAgentSession+ensureAgentSession =+ readAgentSessionKey >>= \case+ Just sessKey -> do+ logLocM DebugS "Found agent session key"+ let handler e = do+ logLocM WarningS $ "Failed to check whether session token matches cluster join token. Using old session. Remove session.key if you need to force a session update. Exception: " <> logStr (displayException e)+ pure sessKey+ safeLiftedHandle handler $ do+ cjt <- getClusterJoinTokenId+ logLocM DebugS $ "Found clusterJoinTokenId " <> logStr cjt+ scjt <- getSessionClusterJoinTokenId sessKey+ logLocM DebugS $ "Found sessionClusterJoinTokenId " <> logStr scjt+ if cjt == scjt+ then pure sessKey+ else do+ logLocM DebugS "Getting new session key to match cluster join token..."+ updateAgentSession+ Nothing -> do+ writeAgentSessionKey "x" -- Sanity check+ logLocM DebugS "Creating agent session"+ updateAgentSession updateAgentSession :: App Text updateAgentSession = do
hercules-ci-agent/Hercules/Agent/WorkerProcess.hs view
@@ -30,11 +30,10 @@ import System.Timeout (timeout) import Prelude () -data WorkerException- = WorkerException- { originalException :: SomeException,- exitStatus :: Maybe ExitCode- }+data WorkerException = WorkerException+ { originalException :: SomeException,+ exitStatus :: Maybe ExitCode+ } deriving (Show, Typeable) instance Exception WorkerException where@@ -44,10 +43,9 @@ Nothing -> "" Just s -> " (worker: " <> show s <> ")" -data WorkerEnvSettings- = WorkerEnvSettings- { nixPath :: [EvaluateTask.NixPathElement (EvaluateTask.SubPathOf FilePath)]- }+data WorkerEnvSettings = WorkerEnvSettings+ { nixPath :: [EvaluateTask.NixPathElement (EvaluateTask.SubPathOf FilePath)]+ } -- | Filter out impure env vars by wildcard, set NIX_PATH modifyEnv :: WorkerEnvSettings -> Map [Char] [Char] -> Map [Char] [Char]@@ -84,40 +82,41 @@ std_out = CreatePipe, std_err = CreatePipe }- liftIO $ withCreateProcess createProcessSpec $ \mIn mOut mErr processHandle -> do- (inHandle, outHandle, errHandle) <-- case (,,) <$> mIn <*> mOut <*> mErr of- Just x -> pure x- Nothing ->- throwIO $- mkIOError- illegalOperationErrorType- "Process did not return all handles"- Nothing -- no handle- Nothing -- no path- pidMaybe <- liftIO $ getPid processHandle- let pid = case pidMaybe of Just x -> fromIntegral x; Nothing -> 0- let stderrPiper =- liftIO $- runConduit- (sourceHandle errHandle .| linesUnboundedAsciiC .| awaitForever (liftIO . unlift . stderrLineHandler pid))- let eventConduit = sourceHandle outHandle .| conduitDecode- commandConduit =- sourceChan commandChan- .| conduitEncode- .| concatMapC (\x -> [Chunk x, Flush])- .| handleC handleEPIPE (sinkHandleFlush inHandle)- handleEPIPE e | ioeGetErrorType e == ResourceVanished = pure ()- handleEPIPE e = throwIO e- let cmdThread = runConduit commandConduit `finally` hClose outHandle- eventThread = unlift $ conduitToCallbacks eventConduit eventHandler- -- plain forkIO so it can process all of stderr in case of an exception- void $ forkIO stderrPiper- withAsync (waitForProcess processHandle) $ \exitAsync -> do- withAsync cmdThread $ \_ -> do- eventThread- `Safe.catch` \e -> do- let oneSecond = 1000 * 1000- maybeStatus <- timeout (5 * oneSecond) (wait exitAsync)- throwIO $ WorkerException e maybeStatus- wait exitAsync+ liftIO $+ withCreateProcess createProcessSpec $ \mIn mOut mErr processHandle -> do+ (inHandle, outHandle, errHandle) <-+ case (,,) <$> mIn <*> mOut <*> mErr of+ Just x -> pure x+ Nothing ->+ throwIO $+ mkIOError+ illegalOperationErrorType+ "Process did not return all handles"+ Nothing -- no handle+ Nothing -- no path+ pidMaybe <- liftIO $ getPid processHandle+ let pid = maybe 0 fromIntegral pidMaybe+ let stderrPiper =+ liftIO $+ runConduit+ (sourceHandle errHandle .| linesUnboundedAsciiC .| awaitForever (liftIO . unlift . stderrLineHandler pid))+ let eventConduit = sourceHandle outHandle .| conduitDecode+ commandConduit =+ sourceChan commandChan+ .| conduitEncode+ .| concatMapC (\x -> [Chunk x, Flush])+ .| handleC handleEPIPE (sinkHandleFlush inHandle)+ handleEPIPE e | ioeGetErrorType e == ResourceVanished = pure ()+ handleEPIPE e = throwIO e+ let cmdThread = runConduit commandConduit `finally` hClose outHandle+ eventThread = unlift $ conduitToCallbacks eventConduit eventHandler+ -- plain forkIO so it can process all of stderr in case of an exception+ void $ forkIO stderrPiper+ withAsync (waitForProcess processHandle) $ \exitAsync -> do+ withAsync cmdThread $ \_ -> do+ eventThread+ `Safe.catch` \e -> do+ let oneSecond = 1000 * 1000+ maybeStatus <- timeout (5 * oneSecond) (wait exitAsync)+ throwIO $ WorkerException e maybeStatus+ wait exitAsync
− src-cnix/CNix.hs
@@ -1,263 +0,0 @@-{-# LANGUAGE QuasiQuotes #-}-{-# LANGUAGE TemplateHaskell #-}--module CNix- ( module CNix,- RawValue,- rawValueType,- module CNix.Internal.Store,- module CNix.Internal.Typed,- type NixStore,- type EvalState,- type Ref,- type SecretKey,- )-where---- TODO: No more Ptr EvalState--- TODO: No more NixStore when EvalState is already there--- TODO: Map Nix-specific C++ exceptions to a CNix exception type-import CNix.Internal.Context-import CNix.Internal.Raw-import CNix.Internal.Store-import CNix.Internal.Typed-import Conduit-import qualified Data.Map as M-import qualified Foreign.C.String-import qualified Language.C.Inline.Cpp as C-import qualified Language.C.Inline.Cpp.Exceptions as C-import Protolude hiding (evalState, throwIO)--newtype Bindings = Bindings (Ptr Bindings')--C.context context--C.include "<stdio.h>"--C.include "<cstring>"--C.include "<math.h>"--C.include "<nix/config.h>"--C.include "<nix/shared.hh>"--C.include "<nix/eval.hh>"--C.include "<nix/eval-inline.hh>"--C.include "<nix/store-api.hh>"--C.include "<nix/common-eval-args.hh>"--C.include "<nix/get-drvs.hh>"--C.include "<nix/derivations.hh>"--C.include "<nix/affinity.hh>"--C.include "<nix/globals.hh>"--C.include "hercules-aliases.h"--C.include "<gc/gc.h>"--C.include "<gc/gc_cpp.h>"--C.include "<gc/gc_allocator.h>"--C.using "namespace nix"--C.verbatim "\nGC_API void GC_CALL GC_throw_bad_alloc() { throw std::bad_alloc(); }\n"--init :: IO ()-init =- void- [C.throwBlock| void {- nix::initNix();- nix::initGC();- } |]--setTalkative :: IO ()-setTalkative =- [C.throwBlock| void {- nix::verbosity = nix::lvlTalkative;- } |]--setDebug :: IO ()-setDebug =- [C.throwBlock| void {- nix::verbosity = nix::lvlVomit;- } |]--setGlobalOption :: Text -> Text -> IO ()-setGlobalOption opt value = do- let optionStr = encodeUtf8 opt- valueStr = encodeUtf8 value- [C.throwBlock| void {- globalConfig.set($bs-cstr:optionStr, $bs-cstr:valueStr);- }|]--setOption :: Text -> Text -> IO ()-setOption opt value = do- let optionStr = encodeUtf8 opt- valueStr = encodeUtf8 value- [C.throwBlock| void {- settings.set($bs-cstr:optionStr, $bs-cstr:valueStr);- }|]--logInfo :: Text -> IO ()-logInfo t = do- let bstr = encodeUtf8 t- [C.throwBlock| void {- printInfo($bs-cstr:bstr);- }|]--mkBindings :: Ptr Bindings' -> IO Bindings-mkBindings p = pure $ Bindings p--withEvalState ::- MonadResource m =>- Ptr (Ref NixStore) ->- (Ptr EvalState -> ConduitT i o m r) ->- ConduitT i o m r-withEvalState store =- bracketP- ( liftIO $- [C.throwBlock| EvalState* {- Strings searchPaths;- return new EvalState(searchPaths, *$(refStore* store));- } |]- )- (\x -> liftIO $ [C.throwBlock| void { delete $(EvalState* x); } |])--evalFile :: Ptr EvalState -> FilePath -> IO RawValue-evalFile evalState filename = do- filename' <- Foreign.C.String.newCString filename- mkRawValue- =<< [C.throwBlock| Value* {- Value value;- $(EvalState *evalState)->evalFile($(const char *filename'), value);- return new (NoGC) Value(value);- }|]---- leaks-newStrings :: IO (Ptr Strings)-newStrings = [C.exp| Strings* { new (NoGC) Strings() }|]--appendString :: Ptr Strings -> ByteString -> IO ()-appendString ss s =- [C.block| void {- $(Strings *ss)->push_back($bs-ptr:s);- }|]--evalArgs :: Ptr EvalState -> [ByteString] -> IO Bindings-evalArgs evalState args = do- argsStrings <- newStrings- forM_ args $ appendString argsStrings- mkBindings- =<< [C.throwBlock| Bindings * {- Strings *args = $(Strings *argsStrings);- struct MixEvalArgs evalArgs;- Bindings *autoArgs;- EvalState &state = *$(EvalState *evalState);-- evalArgs.parseCmdline(*args);- autoArgs = evalArgs.getAutoArgs(state);- if (autoArgs == NULL) {- //cerr << "Could not evaluate automatic arguments" << endl;- abort(); // FIXME- }- return autoArgs;-- // catch (EvalError & e) {- // catch (Error & e) {- // catch (std::exception & e) {-- }|]--autoCallFunction :: Ptr EvalState -> RawValue -> Bindings -> IO RawValue-autoCallFunction evalState (RawValue fun) (Bindings autoArgs) =- mkRawValue- =<< [C.throwBlock| Value* {- Value result;- $(EvalState *evalState)->autoCallFunction(- *$(Bindings *autoArgs),- *$(Value *fun),- result);- return new (NoGC) Value (result);- }|]--isDerivation :: Ptr EvalState -> RawValue -> IO Bool-isDerivation evalState (RawValue v) =- (0 /=)- <$> [C.throwBlock| int {- if ($(Value *v) == NULL) { throw std::invalid_argument("forceValue value must be non-null"); }- return $(EvalState *evalState)->isDerivation(*$(Value *v));- }|]--isFunctor :: Ptr EvalState -> RawValue -> IO Bool-isFunctor evalState (RawValue v) =- (0 /=)- <$> [C.throwBlock| int {- if ($(Value *v) == NULL) { throw std::invalid_argument("forceValue value must be non-null"); }- return $(EvalState *evalState)->isFunctor(*$(Value *v));- }|]--getRecurseForDerivations :: Ptr EvalState -> Value NixAttrs -> IO Bool-getRecurseForDerivations evalState (Value (RawValue v)) =- (0 /=)- <$> [C.throwBlock| int {- Value *v = $(Value *v);- EvalState *evalState = $(EvalState *evalState);- Symbol rfd = evalState->symbols.create("recurseForDerivations");- Bindings::iterator iter = v->attrs->find(rfd);- if (iter == v->attrs->end()) {- return 0;- } else {- evalState->forceValue(*iter->value);- if (iter->value->type == ValueType::tBool) {- return iter->value->boolean ? 1 : 0;- } else {- // They can be empty attrsets???- // Observed in nixpkgs master 67e2de195a4aa0a50ffb1e1ba0b4fb531dca67dc- return 1;- }- }- } |]--getAttrBindings :: Value NixAttrs -> IO Bindings-getAttrBindings (Value (RawValue v)) = mkBindings =<< [C.exp| Bindings *{ $(Value *v)->attrs } |]--getAttrs :: Value NixAttrs -> IO (Map ByteString RawValue)-getAttrs (Value (RawValue v)) = do- begin <- [C.exp| Attr *{ $(Value *v)->attrs->begin() }|]- end <- [C.exp| Attr *{ $(Value *v)->attrs->end() }|]- let gather :: Map ByteString RawValue -> Ptr Attr' -> IO (Map ByteString RawValue)- gather acc i | i == end = pure acc- gather acc i- | otherwise =- do- name <- unsafeMallocBS [C.exp| const char *{ strdup(static_cast<std::string>($(Attr *i)->name).c_str()) } |]- value <- mkRawValue =<< [C.exp| Value *{ new (NoGC) Value(*$(Attr *i)->value) } |]- let acc' = M.insert name value acc- seq acc' pass- gather acc' =<< [C.exp| Attr *{ &$(Attr *i)[1] }|]- gather mempty begin--getDrvFile :: MonadIO m => Ptr EvalState -> RawValue -> m ByteString-getDrvFile evalState (RawValue v) =- unsafeMallocBS- [C.throwBlock| const char *{- EvalState &state = *$(EvalState *evalState);- auto drvInfo = getDerivation(state, *$(Value *v), false);- if (!drvInfo)- throw EvalError("Not a valid derivation");-- std::string drvPath = drvInfo->queryDrvPath();-- // write it (?)- auto drv = state.store->derivationFromPath(drvPath);-- return strdup(drvPath.c_str());- }|]
− src-cnix/CNix/Internal/Context.hs
@@ -1,81 +0,0 @@-{-# LANGUAGE EmptyDataDecls #-}-{-# LANGUAGE QuasiQuotes #-}-{-# LANGUAGE TemplateHaskell #-}--module CNix.Internal.Context- ( module CNix.Internal.Context,- NixStore,- )-where--import Cachix.Client.Store.Context (NixStore)-import Data.ByteString.Unsafe (unsafePackMallocCString)-import qualified Data.Map as M-import qualified Foreign.C.String-import Hercules.Agent.StoreFFI (ExceptionPtr)-import qualified Language.C.Inline.Context as C-import qualified Language.C.Inline.Cpp as C-import qualified Language.C.Types as C-import Protolude--data EvalState--data Strings--data Ref a--data Bindings'--data Value'--data Attr'--data HerculesStore--data Derivation--data Fields--data HerculesLoggerEntry--data LogEntryQueue--data StringsIterator--data DerivationOutputsIterator--data DerivationInputsIterator--data StringPairsIterator--data StringPairs--data SecretKey--context :: C.Context-context =- C.cppCtx <> C.fptrCtx- <> C.bsCtx- { C.ctxTypesTable =- M.singleton (C.TypeName "refStore") [t|Ref NixStore|]- <> M.singleton (C.TypeName "EvalState") [t|EvalState|]- <> M.singleton (C.TypeName "Bindings") [t|Bindings'|]- <> M.singleton (C.TypeName "Value") [t|Value'|]- <> M.singleton (C.TypeName "Attr") [t|Attr'|]- <> M.singleton (C.TypeName "Strings") [t|Strings|]- <> M.singleton (C.TypeName "StringsIterator") [t|StringsIterator|]- <> M.singleton (C.TypeName "StringPairs") [t|StringPairs|]- <> M.singleton (C.TypeName "StringPairsIterator") [t|StringPairsIterator|]- <> M.singleton (C.TypeName "refHerculesStore") [t|Ref HerculesStore|]- <> M.singleton (C.TypeName "Derivation") [t|Derivation|]- <> M.singleton (C.TypeName "LoggerFields") [t|Fields|]- <> M.singleton (C.TypeName "HerculesLoggerEntry") [t|HerculesLoggerEntry|]- <> M.singleton (C.TypeName "LogEntryQueue") [t|LogEntryQueue|]- <> M.singleton (C.TypeName "DerivationOutputsIterator") [t|DerivationOutputsIterator|]- <> M.singleton (C.TypeName "DerivationInputsIterator") [t|DerivationInputsIterator|]- <> M.singleton (C.TypeName "exception_ptr") [t|ExceptionPtr|]- <> M.singleton (C.TypeName "SecretKey") [t|SecretKey|]- }--unsafeMallocBS :: MonadIO m => IO Foreign.C.String.CString -> m ByteString-unsafeMallocBS m = liftIO (unsafePackMallocCString =<< m)
− src-cnix/CNix/Internal/Raw.hs
@@ -1,109 +0,0 @@-{-# LANGUAGE QuasiQuotes #-}-{-# LANGUAGE TemplateHaskell #-}--module CNix.Internal.Raw where--import CNix.Internal.Context-import qualified Language.C.Inline.Cpp as C-import qualified Language.C.Inline.Cpp.Exceptions as C-import Protolude hiding (evalState)-import Prelude ()--C.context context--C.include "<nix/config.h>"--C.include "<nix/eval.hh>"--C.include "<nix/eval-inline.hh>"--C.include "hercules-aliases.h"--C.include "<gc/gc.h>"--C.include "<gc/gc_cpp.h>"--C.include "<gc/gc_allocator.h>"--C.using "namespace nix"--newtype RawValue = RawValue (Ptr Value')---- | Takes ownership of the value.-mkRawValue :: Ptr Value' -> IO RawValue-mkRawValue p = pure $ RawValue p---- | Similar to Nix's Value->type but conflates the List variations-data RawValueType- = Int- | Bool- | String- | Path- | Null- | Attrs- | List- | Thunk- | App- | Lambda- | Blackhole- | PrimOp- | PrimOpApp- | External- | Float- | Other- deriving (Generic, Show, Eq, Ord)---- | You may need to 'forceValue' first.-rawValueType :: RawValue -> IO RawValueType-rawValueType (RawValue v) =- f- <$> [C.block| int {- switch ($(Value* v)->type) {- case tInt: return 1;- case tBool: return 2;- case tString: return 3;- case tPath: return 4;- case tNull: return 5;- case tAttrs: return 6;- case tList1: return 7;- case tList2: return 8;- case tListN: return 9;- case tThunk: return 10;- case tApp: return 11;- case tLambda: return 12;- case tBlackhole: return 13;- case tPrimOp: return 14;- case tPrimOpApp: return 15;- case tExternal: return 16;- case tFloat: return 17;- default: return 0;- }- }|]- where- f 1 = Int- f 2 = Bool- f 3 = String- f 4 = Path- f 5 = Null- f 6 = Attrs- f 7 = List- f 8 = List- f 9 = List- f 10 = Thunk- f 11 = App- f 12 = Lambda- f 13 = Blackhole- f 14 = PrimOp- f 15 = PrimOpApp- f 16 = External- f 17 = Float- f _ = Other--forceValue :: Exception a => Ptr EvalState -> RawValue -> IO (Either a ())-forceValue evalState (RawValue v) =- try- [C.catchBlock| {- Value *v = $(Value *v);- if (v == NULL) throw std::invalid_argument("forceValue value must be non-null");- $(EvalState *evalState)->forceValue(*v);- }|]
− src-cnix/CNix/Internal/Store.hs
@@ -1,415 +0,0 @@-{-# LANGUAGE QuasiQuotes #-}-{-# LANGUAGE TemplateHaskell #-}--module CNix.Internal.Store- ( module CNix.Internal.Store,- HerculesStore,- )-where--import CNix.Internal.Context-import Control.Exception-import Control.Monad.IO.Unlift-import qualified Data.ByteString.Unsafe as BS-import Data.Coerce-import qualified Data.Map as M-import Foreign.C.String (withCString)-import Foreign.ForeignPtr-import Foreign.StablePtr-import Hercules.Agent.StoreFFI-import qualified Language.C.Inline.Cpp as C-import qualified Language.C.Inline.Cpp.Exceptions as C-import Protolude-import System.IO.Unsafe (unsafePerformIO)-import Prelude ()--C.context context--C.include "<cstring>"--C.include "<nix/config.h>"--C.include "<nix/shared.hh>"--C.include "<nix/store-api.hh>"--C.include "<nix/get-drvs.hh>"--C.include "<nix/derivations.hh>"--C.include "<nix/affinity.hh>"--C.include "<nix/globals.hh>"--C.include "hercules-aliases.h"--C.using "namespace nix"--withStore :: MonadUnliftIO m => (Ptr (Ref NixStore) -> m a) -> m a-withStore m = do- UnliftIO ul <- askUnliftIO- liftIO $ withStore' $ \a -> ul (m a)--withStore' ::- (Ptr (Ref NixStore) -> IO r) ->- IO r-withStore' =- bracket- ( liftIO $- [C.throwBlock| refStore* {- refStore s = openStore();- return new refStore(s);- } |]- )- (\x -> liftIO $ [C.exp| void { delete $(refStore* x) } |])--withStoreFromURI ::- MonadUnliftIO m =>- Text ->- (Ptr (Ref NixStore) -> m r) ->- m r-withStoreFromURI storeURIText f = do- let storeURI = encodeUtf8 storeURIText- (UnliftIO unlift) <- askUnliftIO- liftIO $- bracket- [C.throwBlock| refStore* {- refStore s = openStore($bs-cstr:storeURI);- return new refStore(s);- }|]- (\x -> [C.exp| void { delete $(refStore* x) } |])- (unlift . f)--storeUri :: MonadIO m => Ptr (Ref NixStore) -> m ByteString-storeUri store =- unsafeMallocBS- [C.block| const char* {- std::string uri = (*$(refStore* store))->getUri();- return strdup(uri.c_str());- } |]--ensurePath :: Ptr (Ref NixStore) -> ByteString -> IO ()-ensurePath store path =- [C.throwBlock| void {- (*$(refStore* store))->ensurePath(std::string($bs-ptr:path, $bs-len:path));- } |]--clearPathInfoCache :: Ptr (Ref NixStore) -> IO ()-clearPathInfoCache store =- [C.throwBlock| void {- (*$(refStore* store))->clearPathInfoCache();- } |]--clearSubstituterCaches :: IO ()-clearSubstituterCaches =- [C.throwBlock| void {- auto subs = nix::getDefaultSubstituters();- for (auto sub : subs) {- sub->clearPathInfoCache();- }- } |]--buildPath :: Ptr (Ref NixStore) -> ByteString -> IO ()-buildPath store path =- [C.throwBlock| void {- PathSet ps({std::string($bs-ptr:path, $bs-len:path)});- (*$(refStore* store))->buildPaths(ps);- } |]--getDerivation :: Ptr (Ref NixStore) -> ByteString -> IO (ForeignPtr Derivation)-getDerivation store path = do- ptr <-- [C.throwBlock| Derivation *{- return new Derivation(- (*$(refStore* store))->derivationFromPath(std::string($bs-ptr:path, $bs-len:path))- );- } |]- newForeignPtr finalizeDerivation ptr---- Useful for testingg-getDerivationFromFile :: ByteString -> IO (ForeignPtr Derivation)-getDerivationFromFile path = do- ptr <-- [C.throwBlock| Derivation *{- return new Derivation(- readDerivation(std::string($bs-ptr:path, $bs-len:path))- );- } |]- newForeignPtr finalizeDerivation ptr--finalizeDerivation :: FinalizerPtr Derivation-{-# NOINLINE finalizeDerivation #-}-finalizeDerivation =- unsafePerformIO- [C.exp|- void (*)(Derivation *) {- [](Derivation *v) {- delete v;- }- } |]---- | Throws when missing-getDerivationOutputPath :: ForeignPtr Derivation -> ByteString -> IO ByteString-getDerivationOutputPath fd outputName = withForeignPtr fd $ \d ->- [C.throwBlock|- const char *{- std::string outputName($bs-ptr:outputName, $bs-len:outputName);- Derivation *d = $(Derivation *d);- return strdup(d->outputs.at(outputName).path.c_str());- }- |]- >>= BS.unsafePackMallocCString--data DerivationOutput- = DerivationOutput- { derivationOutputName :: !ByteString,- derivationOutputPath :: !ByteString,- derivationOutputHashAlgo :: !ByteString,- derivationOutputHash :: !ByteString- }--getDerivationOutputs :: ForeignPtr Derivation -> IO [DerivationOutput]-getDerivationOutputs derivation =- bracket- [C.exp| DerivationOutputsIterator* {- new DerivationOutputsIterator($fptr-ptr:(Derivation *derivation)->outputs.begin())- }|]- deleteDerivationOutputsIterator- $ \i -> fix $ \continue -> do- isEnd <- (0 /=) <$> [C.exp| bool { *$(DerivationOutputsIterator *i) == $fptr-ptr:(Derivation *derivation)->outputs.end() }|]- if isEnd- then pure []- else do- name <- [C.exp| const char*{ strdup((*$(DerivationOutputsIterator *i))->first.c_str()) }|] >>= BS.unsafePackMallocCString- path <- [C.exp| const char*{ strdup((*$(DerivationOutputsIterator *i))->second.path.c_str()) }|] >>= BS.unsafePackMallocCString- hash_ <- [C.exp| const char*{ strdup((*$(DerivationOutputsIterator *i))->second.hash.c_str()) }|] >>= BS.unsafePackMallocCString- hashAlgo <- [C.exp| const char*{ strdup((*$(DerivationOutputsIterator *i))->second.hashAlgo.c_str()) }|] >>= BS.unsafePackMallocCString- [C.block| void { (*$(DerivationOutputsIterator *i))++; }|]- (DerivationOutput name path hashAlgo hash_ :) <$> continue--deleteDerivationOutputsIterator :: Ptr DerivationOutputsIterator -> IO ()-deleteDerivationOutputsIterator a = [C.block| void { delete $(DerivationOutputsIterator *a); }|]--getDerivationPlatform :: ForeignPtr Derivation -> IO ByteString-getDerivationPlatform derivation =- unsafeMallocBS- [C.exp| const char* {- strdup($fptr-ptr:(Derivation *derivation)->platform.c_str())- } |]--getDerivationSources :: ForeignPtr Derivation -> IO [ByteString]-getDerivationSources derivation =- bracket- [C.throwBlock| Strings* {- Strings *r = new Strings();- for (auto i : $fptr-ptr:(Derivation *derivation)->inputSrcs) {- r->push_back(i);- }- return r;- }|]- deleteStrings- toByteStrings--getDerivationInputs :: ForeignPtr Derivation -> IO [(ByteString, [ByteString])]-getDerivationInputs derivation =- bracket- [C.exp| DerivationInputsIterator* {- new DerivationInputsIterator($fptr-ptr:(Derivation *derivation)->inputDrvs.begin())- }|]- deleteDerivationInputsIterator- $ \i -> fix $ \continue -> do- isEnd <- (0 /=) <$> [C.exp| bool { *$(DerivationInputsIterator *i) == $fptr-ptr:(Derivation *derivation)->inputDrvs.end() }|]- if isEnd- then pure []- else do- name <- [C.exp| const char*{ strdup((*$(DerivationInputsIterator *i))->first.c_str()) }|] >>= BS.unsafePackMallocCString- outs <-- bracket- [C.block| Strings*{ - Strings *r = new Strings();- for (auto i : (*$(DerivationInputsIterator *i))->second) {- r->push_back(i);- }- return r;- }|]- deleteStrings- toByteStrings- [C.block| void { (*$(DerivationInputsIterator *i))++; }|]- ((name, outs) :) <$> continue--deleteDerivationInputsIterator :: Ptr DerivationInputsIterator -> IO ()-deleteDerivationInputsIterator a = [C.block| void { delete $(DerivationInputsIterator *a); }|]--getDerivationEnv :: ForeignPtr Derivation -> IO (Map ByteString ByteString)-getDerivationEnv derivation =- [C.exp| StringPairs* { &($fptr-ptr:(Derivation *derivation)->env) }|]- >>= toByteStringMap--getDerivationOutputNames :: ForeignPtr Derivation -> IO [ByteString]-getDerivationOutputNames derivation =- bracket- [C.throwBlock| Strings* {- Strings *r = new Strings();- for (auto i : $fptr-ptr:(Derivation *derivation)->outputs) {- r->push_back(i.first);- }- return r;- }|]- deleteStrings- toByteStrings--deleteStringPairs :: Ptr StringPairs -> IO ()-deleteStringPairs s = [C.block| void { delete $(StringPairs *s); }|]--deleteStrings :: Ptr Strings -> IO ()-deleteStrings s = [C.block| void { delete $(Strings *s); }|]--finalizeStrings :: FinalizerPtr Strings-{-# NOINLINE finalizeStrings #-}-finalizeStrings =- unsafePerformIO- [C.exp|- void (*)(Strings *) {- [](Strings *v) {- delete v;- }- } |]--getStringsLength :: Ptr Strings -> IO C.CSize-getStringsLength strings = [C.exp| size_t { $(Strings *strings)->size() }|]--toByteStrings :: Ptr Strings -> IO [ByteString]-toByteStrings strings = do- i <- [C.exp| StringsIterator *{ new StringsIterator($(Strings *strings)->begin()) } |]- fix $ \go -> do- isEnd <- (0 /=) <$> [C.exp| bool { *$(StringsIterator *i) == $(Strings *strings)->end() }|]- if isEnd- then pure []- else do- s <- [C.exp| const char*{ strdup((*$(StringsIterator *i))->c_str()) }|]- bs <- BS.unsafePackMallocCString s- [C.block| void { (*$(StringsIterator *i))++; }|]- (bs :) <$> go--toByteStringMap :: Ptr StringPairs -> IO (Map ByteString ByteString)-toByteStringMap strings = M.fromList <$> do- i <- [C.exp| StringPairsIterator *{ new StringPairsIterator($(StringPairs *strings)->begin()) } |]- fix $ \go -> do- isEnd <- (0 /=) <$> [C.exp| bool { *$(StringPairsIterator *i) == $(StringPairs *strings)->end() }|]- if isEnd- then pure []- else do- k <- [C.exp| const char*{ strdup((*$(StringPairsIterator *i))->first.c_str()) }|]- v <- [C.exp| const char*{ strdup((*$(StringPairsIterator *i))->second.c_str()) }|]- bk <- BS.unsafePackMallocCString k- bv <- BS.unsafePackMallocCString v- [C.block| void { (*$(StringPairsIterator *i))++; }|]- ((bk, bv) :) <$> go--withStrings :: (Ptr Strings -> IO a) -> IO a-withStrings =- bracket- [C.exp| Strings *{ new Strings() }|]- (\sp -> [C.block| void { delete $(Strings *sp); }|])--pushString :: Ptr Strings -> ByteString -> IO ()-pushString strings s =- [C.block| void { $(Strings *strings)->push_back($bs-cstr:s); }|]--copyClosure :: Ptr (Ref NixStore) -> Ptr (Ref NixStore) -> [ByteString] -> IO ()-copyClosure src dest paths = do- withStrings $ \pathStrings -> do- paths & traverse_ (pushString pathStrings)- [C.throwBlock| void {- StringSet pathSet;- for (auto path : *$(Strings *pathStrings)) {- pathSet.insert(path);- }- nix::copyClosure(*$(refStore* src), *$(refStore* dest), pathSet);- }|]--parseSecretKey :: ByteString -> IO (ForeignPtr SecretKey)-parseSecretKey bs =- [C.throwBlock| SecretKey* {- return new SecretKey($bs-cstr:bs);- }|]- >>= newForeignPtr finalizeSecretKey--finalizeSecretKey :: FinalizerPtr SecretKey-{-# NOINLINE finalizeSecretKey #-}-finalizeSecretKey =- unsafePerformIO- [C.exp|- void (*)(SecretKey *) {- [](SecretKey *v) {- delete v;- }- } |]--signPath ::- Ptr (Ref NixStore) ->- -- | Secret signing key- Ptr SecretKey ->- -- | Store path- ByteString ->- -- | False if the signature was already present, True if the signature was added- IO Bool-signPath store secretKey path = (== 1) <$> do- [C.throwBlock| int {- nix::ref<nix::Store> store = *$(refStore *store);- std::string storePath($bs-cstr:path);- auto currentInfo = store->queryPathInfo(storePath);-- auto info2(*currentInfo);- info2.sigs.clear();- info2.sign(*$(SecretKey *secretKey));- assert(!info2.sigs.empty());- auto sig = *info2.sigs.begin();-- if (currentInfo->sigs.count(sig)) {- return 0;- } else {- store->addSignatures(storePath, info2.sigs);- return 1;- }- }|]------- Hercules ------withHerculesStore ::- Ptr (Ref NixStore) ->- (Ptr (Ref HerculesStore) -> IO a) ->- IO a-withHerculesStore wrappedStore =- bracket- ( liftIO $- [C.block| refHerculesStore* {- refStore &s = *$(refStore *wrappedStore);- refHerculesStore hs(new HerculesStore({}, s));- return new refHerculesStore(hs);- } |]- )- (\x -> liftIO $ [C.exp| void { delete $(refHerculesStore* x) } |])--nixStore :: Ptr (Ref HerculesStore) -> Ptr (Ref NixStore)-nixStore = coerce--printDiagnostics :: Ptr (Ref HerculesStore) -> IO ()-printDiagnostics s =- [C.throwBlock| void{- (*$(refHerculesStore* s))->printDiagnostics();- }|]---- TODO catch pure exceptions from displayException-setBuilderCallback :: Ptr (Ref HerculesStore) -> (ByteString -> IO ()) -> IO ()-setBuilderCallback s callback = do- p <-- mkBuilderCallback $ \cstr exceptionToThrowPtr ->- Control.Exception.catch (BS.unsafePackMallocCString cstr >>= callback) $ \e ->- withCString (displayException (e :: SomeException)) $ \renderedException -> do- stablePtr <- castStablePtrToPtr <$> newStablePtr e- [C.block| void {- (*$(exception_ptr *exceptionToThrowPtr)) = std::make_exception_ptr(HaskellException(std::string($(const char* renderedException)), $(void* stablePtr)));- }|]- [C.throwBlock| void {- (*$(refHerculesStore* s))->setBuilderCallback($(void (*p)(const char *, exception_ptr *) ));- }|]
− src-cnix/CNix/Internal/Typed.hs
@@ -1,77 +0,0 @@-{-# LANGUAGE EmptyDataDecls #-}--module CNix.Internal.Typed where--import CNix.Internal.Context-import CNix.Internal.Raw-import Protolude hiding- ( evalState,- throwIO,- )-import Prelude (userError)---- | Runtime-Typed Value. This implies that it has been forced,--- because otherwise the type would not be known.-newtype Value a = Value {rtValue :: RawValue}--data NixInt--data NixFloat--data NixString--data NixPath--data NixAttrs--data NixFunction--data NixList--data NixPrimOp--data NixPrimOpApp--data NixExternal---- TODO: actually encapsulate the constructor-unsafeAssertType :: RawValue -> Value a-unsafeAssertType = Value---- This is useful because you regain exhaustiveness checking.--- Otherwise a bunch of downcast functions might do.-data Match- = IsInt (Value NixInt)- | IsBool (Value Bool)- | IsString (Value NixString)- | IsPath (Value NixPath)- | IsNull (Value ())- | IsAttrs (Value NixAttrs)- | IsList (Value NixList)- | IsFunction (Value NixFunction)- | IsExternal (Value NixExternal)- | IsFloat (Value NixFloat)---- FIXME: errors don't provide any clue here-match :: Ptr EvalState -> RawValue -> IO (Either SomeException Match)-match es v = forceValue es v >>= \case- Left e -> pure (Left e)- Right _ -> rawValueType v <&> \case- Int -> pure $ IsInt $ unsafeAssertType v- Bool -> pure $ IsBool $ unsafeAssertType v- String -> pure $ IsString $ unsafeAssertType v- Path -> pure $ IsPath $ unsafeAssertType v- Null -> pure $ IsNull $ unsafeAssertType v- Attrs -> pure $ IsAttrs $ unsafeAssertType v- List -> pure $ IsList $ unsafeAssertType v- Thunk -> Left $ SomeException $ userError "Could not force Nix thunk" -- FIXME: custom exception?- App -> Left $ SomeException $ userError "Could not force Nix thunk (App)"- Blackhole ->- Left $ SomeException $ userError "Could not force Nix thunk (Blackhole)"- Lambda -> pure $ IsFunction $ unsafeAssertType v- PrimOp -> pure $ IsFunction $ unsafeAssertType v- PrimOpApp -> pure $ IsFunction $ unsafeAssertType v- External -> pure $ IsExternal $ unsafeAssertType v- Float -> pure $ IsFloat $ unsafeAssertType v- Other ->- Left $ SomeException $ userError "Unknown runtime type in Nix value"
− src-internal-ffi/Hercules/Agent/StoreFFI.hs
@@ -1,20 +0,0 @@-{-# OPTIONS_GHC -fobject-code #-}-{-# LANGUAGE CPP #-}--module Hercules.Agent.StoreFFI where--import qualified Foreign.C-import Protolude--type BuilderCallback = Foreign.C.CString -> Ptr ExceptionPtr -> IO ()---- Work around a problem in ghcide with foreign imports.-#ifndef GHCIDE-foreign import ccall "wrapper"- mkBuilderCallback :: BuilderCallback -> IO (FunPtr BuilderCallback)-#else-mkBuilderCallback :: BuilderCallback -> IO (FunPtr BuilderCallback)-mkBuilderCallback = panic "This is a stub to work around a ghcide issue. Please compile without -DGHCIDE"-#endif--data ExceptionPtr
src/Data/Conduit/Katip/Orphans.hs view
@@ -7,13 +7,11 @@ import Katip instance Katip m => Katip (ConduitT i o m) where- getLogEnv = lift getLogEnv localLogEnv f = transPipe (localLogEnv f) instance KatipContext m => KatipContext (ConduitT i o m) where- getKatipContext = lift getKatipContext getKatipNamespace = lift getKatipNamespace
+ src/Hercules/Agent/NixFile.hs view
@@ -0,0 +1,44 @@+module Hercules.Agent.NixFile+ ( findNixFile,+ )+where++import Protolude+import qualified System.Directory as Dir+import System.FilePath ((</>))++type Ambiguity = [FilePath]++searchPath :: [Ambiguity]+searchPath = [["nix/ci.nix", "ci.nix"], ["default.nix"]]++findNixFile :: FilePath -> IO (Either Text FilePath)+findNixFile projectDir = do+ searchResult <-+ for searchPath $+ traverse $ \relPath ->+ let path = projectDir </> relPath+ in Dir.doesFileExist path >>= \case+ True -> pure $ Just (relPath, path)+ False -> pure Nothing+ case filter (not . null) $ map catMaybes searchResult of+ [(_relPath, unambiguous)] : _ -> pure (pure unambiguous)+ ambiguous : _ ->+ pure $+ Left $+ "Don't know what to do, expecting only one of "+ <> englishConjunction "or" (map fst ambiguous)+ [] ->+ pure $+ Left $+ "Please provide a Nix expression to build. Could not find any of "+ <> englishConjunction "or" (concat searchPath)+ <> " in your source"++englishConjunction :: Show a => Text -> [a] -> Text+englishConjunction _ [] = "none"+englishConjunction _ [a] = show a+englishConjunction connective [a1, a2] =+ show a1 <> " " <> connective <> " " <> show a2+englishConjunction connective (a : as) =+ show a <> ", " <> englishConjunction connective as
src/Hercules/Agent/Producer.hs view
@@ -20,11 +20,10 @@ -- | A thread producing zero or more payloads and a final value. -- Handles exception propagation.-data Producer p r- = Producer- { producerQueueRead :: STM (Msg p r),- producerThread :: ThreadId- }+data Producer p r = Producer+ { producerQueueRead :: STM (Msg p r),+ producerThread :: ThreadId+ } deriving (Functor) data ProducerCancelled = ProducerCancelled@@ -164,13 +163,13 @@ doPerformBatch buf beginReading in beginReading- liftIO- $ withAsync+ liftIO $+ withAsync ( forever $ do threadDelay maxDelay atomically $ writeTQueue flushes () )- $ \_flusher -> unlift $ withProducer producer f+ $ \_flusher -> unlift $ withProducer producer f syncer :: MonadIO m => (Syncing a -> m ()) -> m () syncer writer = do
+ src/Hercules/Agent/Sensitive.hs view
@@ -0,0 +1,22 @@+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE DerivingVia #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}++module Hercules.Agent.Sensitive where++import Data.Binary+import Protolude+import Text.Show++-- | newtype wrapper to avoid leaking sensitive data through Show+newtype Sensitive a = Sensitive {reveal :: a}+ deriving (Generic)+ deriving newtype (Binary, Eq, Ord)+ deriving (Functor, Applicative, Monad) via Identity++-- | @const "<sensitive>"@+instance Show (Sensitive a) where+ show _ = "<sensitive>"++revealContainer :: Functor f => Sensitive (f a) -> f (Sensitive a)+revealContainer (Sensitive fa) = Sensitive <$> fa
src/Hercules/Agent/Socket.hs view
@@ -24,8 +24,8 @@ import Data.Time.Extras import Hercules.API.Agent.LifeCycle.ServiceInfo (ServiceInfo) import qualified Hercules.API.Agent.LifeCycle.ServiceInfo as ServiceInfo-import qualified Hercules.API.Agent.Socket.Frame as Frame import Hercules.API.Agent.Socket.Frame (Frame)+import qualified Hercules.API.Agent.Socket.Frame as Frame import Hercules.Agent.STM (atomically, newTChanIO, newTVarIO) import Katip (KatipContext, Severity (..), katipAddContext, katipAddNamespace, logLocM, sl) import Network.URI (URI, uriAuthority, uriPath, uriPort, uriQuery, uriRegName, uriScheme)@@ -37,25 +37,23 @@ import UnliftIO.Timeout (timeout) import Wuss (runSecureClientWith) -data Socket r w- = Socket- { write :: w -> STM (),- serviceChan :: TChan r,- sync :: STM (STM ())- }+data Socket r w = Socket+ { write :: w -> STM (),+ serviceChan :: TChan r,+ sync :: STM (STM ())+ } syncIO :: Socket r w -> IO () syncIO = join . fmap atomically . atomically . sync -- | Parameters to start 'withReliableSocket'.-data SocketConfig ap sp m- = SocketConfig- { makeHello :: m ap,- checkVersion :: (sp -> m (Either Text ())),- baseURL :: URI,- path :: Text,- token :: ByteString- }+data SocketConfig ap sp m = SocketConfig+ { makeHello :: m ap,+ checkVersion :: (sp -> m (Either Text ())),+ baseURL :: URI,+ path :: Text,+ token :: ByteString+ } requiredServiceVersion :: (Int, Int) requiredServiceVersion = (2, 0)@@ -74,15 +72,16 @@ writeTVar agentMessageNextN (c + 1) pure $ Frame.Msg {n = c, p = p} socketThread = runReliableSocket socketConfig writeQueue serviceMessageChan highestAcked- socket = Socket- { write = tagPayload >=> writeTBQueue writeQueue,- serviceChan = serviceMessageChan,- sync = do- counterAtSyncStart <- (\n -> n - 1) <$> readTVar agentMessageNextN- pure do- acked <- readTVar highestAcked- guard $ acked >= counterAtSyncStart- }+ socket =+ Socket+ { write = tagPayload >=> writeTBQueue writeQueue,+ serviceChan = serviceMessageChan,+ sync = do+ counterAtSyncStart <- (\n -> n - 1) <$> readTVar agentMessageNextN+ pure do+ acked <- readTVar highestAcked+ guard $ acked >= counterAtSyncStart+ } race socketThread (f socket) <&> either identity identity checkVersion' :: Applicative m => ServiceInfo -> m (Either Text ())@@ -136,11 +135,12 @@ handshake conn = katipAddNamespace "Handshake" do siMsg <- recv conn case siMsg of- Frame.Oob {o = o'} -> checkVersion socketConfig o' >>= \case- Left e -> do- send conn [Frame.Exception e]- throwIO $ FatalError "It looks like you're running a development version of hercules-ci-agent that is not yet supported on hercules-ci.com. Please use the stable branch or a tag."- Right _ -> pass+ Frame.Oob {o = o'} ->+ checkVersion socketConfig o' >>= \case+ Left e -> do+ send conn [Frame.Exception e]+ throwIO $ FatalError "It looks like you're running a development version of hercules-ci-agent that is not yet supported on hercules-ci.com. Please use the stable branch or a tag."+ Right _ -> pass _ -> throwIO $ FatalError "Unexpected message. This is either a bug or you might need to update your agent." hello <- makeHello socketConfig send conn [Frame.Oob hello]@@ -221,13 +221,13 @@ -- terminate other threads via race_ pass else noAckCleanupThread' expectedN- forever- $ handle logWarningPause- $ withConnection' socketConfig- $ \conn -> do- katipAddNamespace "Handshake" do- handshake conn- readThread conn `race_` writeThread conn `race_` noAckCleanupThread+ forever $+ handle logWarningPause $+ withConnection' socketConfig $+ \conn -> do+ katipAddNamespace "Handshake" do+ handshake conn+ readThread conn `race_` writeThread conn `race_` noAckCleanupThread msgN :: Frame o a -> Maybe Integer msgN (Frame.Msg {n = n}) = Just n@@ -262,6 +262,7 @@ withTimeout :: (Exception e, MonadIO m, MonadUnliftIO m) => NominalDiffTime -> e -> m a -> m a withTimeout t e _ | t <= 0 = throwIO e-withTimeout t e m = timeout (ceiling $ t * 1_000_000) m >>= \case- Nothing -> throwIO e- Just a -> pure a+withTimeout t e m =+ timeout (ceiling $ t * 1_000_000) m >>= \case+ Nothing -> throwIO e+ Just a -> pure a
src/Hercules/Agent/WorkerProtocol/Command.hs view
@@ -5,6 +5,7 @@ import Data.Binary import qualified Hercules.Agent.WorkerProtocol.Command.Build as Build import qualified Hercules.Agent.WorkerProtocol.Command.BuildResult as BuildResult+import qualified Hercules.Agent.WorkerProtocol.Command.Effect as Effect import qualified Hercules.Agent.WorkerProtocol.Command.Eval as Eval import Protolude @@ -12,4 +13,5 @@ = Eval Eval.Eval | BuildResult BuildResult.BuildResult | Build Build.Build+ | Effect Effect.Effect deriving (Generic, Binary, Show, Eq)
src/Hercules/Agent/WorkerProtocol/Command/Build.hs view
@@ -6,11 +6,10 @@ import Hercules.Agent.WorkerProtocol.LogSettings import Protolude -data Build- = Build- { drvPath :: Text,- inputDerivationOutputPaths :: [ByteString],- logSettings :: LogSettings,- materializeDerivation :: Bool- }+data Build = Build+ { drvPath :: Text,+ inputDerivationOutputPaths :: [ByteString],+ logSettings :: LogSettings,+ materializeDerivation :: Bool+ } deriving (Generic, Binary, Show, Eq)
src/Hercules/Agent/WorkerProtocol/Command/BuildResult.hs view
@@ -7,11 +7,11 @@ import Protolude data BuildResult = BuildResult Text UUID BuildStatus- deriving (Generic, Binary, Show, Eq)+ deriving (Generic, Binary, Show, Eq, NFData) -- | Subset of @DerivationStatus@, with a @Binary@ instance. data BuildStatus = Success | Failure | DependencyFailure- deriving (Generic, Binary, Show, Eq)+ deriving (Generic, Binary, Show, Eq, NFData)
+ src/Hercules/Agent/WorkerProtocol/Command/Effect.hs view
@@ -0,0 +1,19 @@+{-# LANGUAGE DeriveAnyClass #-}++module Hercules.Agent.WorkerProtocol.Command.Effect where++import Data.Binary+import Hercules.Agent.Sensitive+import Hercules.Agent.WorkerProtocol.LogSettings+import Protolude++data Effect = Effect+ { drvPath :: Text,+ apiBaseURL :: Text,+ logSettings :: LogSettings,+ inputDerivationOutputPaths :: [ByteString],+ materializeDerivation :: Bool,+ secretsPath :: FilePath,+ token :: Sensitive Text+ }+ deriving (Generic, Binary, Show, Eq)
src/Hercules/Agent/WorkerProtocol/Command/Eval.hs view
@@ -6,17 +6,16 @@ import Hercules.Agent.WorkerProtocol.LogSettings import Protolude -data Eval- = Eval- { cwd :: FilePath,- file :: Text,- autoArguments :: Map Text Arg,- -- | NB currently the options will leak from one evaluation to- -- the next if you're running them in the same worker!- -- (as of now, we use one worker process per evaluation)- extraNixOptions :: [(Text, Text)],- logSettings :: LogSettings- }+data Eval = Eval+ { cwd :: FilePath,+ file :: Text,+ autoArguments :: Map Text Arg,+ -- | NB currently the options will leak from one evaluation to+ -- the next if you're running them in the same worker!+ -- (as of now, we use one worker process per evaluation)+ extraNixOptions :: [(Text, Text)],+ logSettings :: LogSettings+ } deriving (Generic, Binary, Show, Eq) data Arg
src/Hercules/Agent/WorkerProtocol/Event.hs view
@@ -17,5 +17,6 @@ | Error Text | Build Text Text (Maybe UUID) | BuildResult BuildResult+ | EffectResult Int | Exception Text deriving (Generic, Binary, Show, Eq)
src/Hercules/Agent/WorkerProtocol/Event/Attribute.hs view
@@ -6,10 +6,17 @@ import Protolude import Prelude () -data Attribute- = Attribute- { path :: [ByteString],- drv :: ByteString- -- TODO: metadata- }+data AttributeType+ = Regular+ | MustFail+ | MayFail+ | DependenciesOnly+ | Effect+ deriving (Generic, Binary, Show, Eq)++data Attribute = Attribute+ { path :: [ByteString],+ drv :: ByteString,+ typ :: AttributeType+ } deriving (Generic, Binary, Show, Eq)
src/Hercules/Agent/WorkerProtocol/Event/AttributeError.hs view
@@ -6,11 +6,10 @@ import Protolude import Prelude () -data AttributeError- = AttributeError- { path :: [ByteString],- message :: Text,- errorType :: Maybe Text,- errorDerivation :: Maybe Text- }+data AttributeError = AttributeError+ { path :: [ByteString],+ message :: Text,+ errorType :: Maybe Text,+ errorDerivation :: Maybe Text+ } deriving (Generic, Binary, Show, Eq)
src/Hercules/Agent/WorkerProtocol/Event/BuildResult.hs view
@@ -10,15 +10,14 @@ | BuildSuccess {outputs :: [OutputInfo]} deriving (Generic, Binary, Show, Eq) -data OutputInfo- = OutputInfo- { -- | e.g. out, dev- name :: ByteString,- -- | store path- path :: ByteString,- -- | typically sha256:...- hash :: ByteString,- -- | nar size in bytes- size :: Int64- }+data OutputInfo = OutputInfo+ { -- | e.g. out, dev+ name :: ByteString,+ -- | store path+ path :: ByteString,+ -- | typically sha256:...+ hash :: ByteString,+ -- | nar size in bytes+ size :: Int64+ } deriving (Generic, Binary, Show, Eq)
src/Hercules/Agent/WorkerProtocol/LogSettings.hs view
@@ -1,23 +1,17 @@ {-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE DerivingVia #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-} module Hercules.Agent.WorkerProtocol.LogSettings where import Data.Binary+import Hercules.Agent.Sensitive import Protolude-import Text.Show -data LogSettings- = LogSettings- { path :: Text,- baseURL :: Text,- token :: Sensitive Text- }+data LogSettings = LogSettings+ { path :: Text,+ baseURL :: Text,+ token :: Sensitive Text+ } deriving (Generic, Binary, Show, Eq)---- | newtype wrapper to avoid leaking sensitive data through Show-newtype Sensitive a = Sensitive {reveal :: a}- deriving (Generic, Binary, Eq, Ord)---- | @const "<sensitive>"@-instance Show (Sensitive a) where- show _ = "<sensitive>"
+ src/Hercules/Effect.hs view
@@ -0,0 +1,140 @@+{-# LANGUAGE BlockArguments #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Hercules.Effect where++import Control.Monad.Catch (MonadThrow)+import qualified Data.Aeson as A+import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy as BL+import qualified Data.Map as M+import Foreign (ForeignPtr)+import Hercules.Agent.Sensitive (Sensitive (Sensitive, reveal), revealContainer)+import Hercules.CNix.Store (getDerivationArguments, getDerivationBuilder, getDerivationEnv)+import Hercules.CNix.Store.Context (Derivation)+import Hercules.Effect.Container (BindMount (BindMount))+import qualified Hercules.Effect.Container as Container+import Hercules.Error (escalateAs)+import qualified Hercules.Formats.Secret as Formats.Secret+import Katip (KatipContext, Severity (..), logLocM, logStr)+import Protolude+import System.FilePath+import UnliftIO.Directory (createDirectory, createDirectoryIfMissing)++parseDrvSecretsMap :: Map ByteString ByteString -> Either Text (Map Text Text)+parseDrvSecretsMap drvEnv =+ case drvEnv & M.lookup "secretsMap" of+ Nothing -> pure mempty+ Just secretsMapText -> case A.eitherDecode (BL.fromStrict secretsMapText) of+ Left _ -> Left "Could not parse secretsMap variable in derivation. It must be a JSON dictionary of strings referencing agent secret names."+ Right r -> Right r++-- | Write secrets to file based on secretsMap value+writeSecrets :: (MonadIO m, KatipContext m) => FilePath -> Map Text Text -> Map Text (Sensitive Formats.Secret.Secret) -> FilePath -> m ()+writeSecrets sourceFile secretsMap extraSecrets destinationDirectory = write . fmap reveal . addExtra =<< gather+ where+ addExtra = flip M.union extraSecrets+ write = liftIO . BS.writeFile (destinationDirectory </> "secrets.json") . BL.toStrict . A.encode+ gather =+ if null secretsMap+ then pure mempty+ else do+ secretsBytes <- liftIO $ BS.readFile sourceFile+ r <- case A.eitherDecode $ BL.fromStrict secretsBytes of+ Left e -> do+ logLocM ErrorS $ "Could not parse secrets file " <> logStr sourceFile <> ": " <> logStr e+ throwIO $ FatalError "Could not parse secrets file as configured on agent."+ Right r -> pure (Sensitive r)+ createDirectoryIfMissing True destinationDirectory+ secretsMap & M.traverseWithKey \destinationName (secretName :: Text) -> do+ case revealContainer (r <&> M.lookup secretName) of+ Nothing ->+ liftIO $+ throwIO $+ FatalError $+ "Secret " <> secretName <> " does not exist, so we can't find a secret for " <> destinationName <> ". Please make sure that the secret name matches a secret on your agents."+ Just ssecret ->+ pure do+ secret <- ssecret+ -- Currently this is `id` but we might want to fork the+ -- format here or omit some fields.+ pure $+ Formats.Secret.Secret+ { data_ = Formats.Secret.data_ secret+ }++runEffect :: (MonadThrow m, KatipContext m) => ForeignPtr Derivation -> Sensitive Text -> FilePath -> Text -> FilePath -> m ExitCode+runEffect derivation token secretsPath apiBaseURL dir = do+ drvBuilder <- liftIO $ getDerivationBuilder derivation+ drvArgs <- liftIO $ getDerivationArguments derivation+ drvEnv <- liftIO $ getDerivationEnv derivation+ drvSecretsMap <- escalateAs FatalError $ parseDrvSecretsMap drvEnv+ let mkDir d = let newDir = dir </> d in toS newDir <$ createDirectory newDir+ buildDir <- mkDir "build"+ etcDir <- mkDir "etc"+ secretsDir <- mkDir "secrets"+ runcDir <- mkDir "runc-state"+ let extraSecrets =+ M.singleton+ "hercules-ci"+ ( do+ tok <- token+ pure $+ Formats.Secret.Secret+ { data_ = M.singleton "token" $ A.String tok+ }+ )+ writeSecrets secretsPath drvSecretsMap extraSecrets (toS secretsDir)+ liftIO $ do+ -- Nix sandbox sets tmp to buildTopDir+ -- Nix sandbox reference: https://github.com/NixOS/nix/blob/24e07c428f21f28df2a41a7a9851d5867f34753a/src/libstore/build.cc#L2545+ --+ -- TODO: what if we have structuredAttrs?+ -- TODO: implement passAsFile?+ let overridableEnv, onlyImpureOverridableEnv, fixedEnv :: Map Text Text+ overridableEnv =+ M.fromList+ [ ("PATH", "/path-not-set"),+ ("HOME", "/homeless-shelter"),+ ("NIX_STORE", "/nix/store"), -- TODO store.storeDir+ ("NIX_BUILD_CORES", "1"), -- not great+ ("NIX_REMOTE", "daemon"),+ ("IN_HERCULES_CI_EFFECT", "true"),+ ("HERCULES_CI_API_BASE_URL", apiBaseURL),+ ("HERCULES_CI_SECRETS_JSON", "/secrets/secrets.json")+ ]+ -- NB: this is lossy. Consider using ByteString-based process functions+ drvEnv' = drvEnv & M.mapKeys (decodeUtf8With lenientDecode) & fmap (decodeUtf8With lenientDecode)+ impureEnvVars = mempty -- TODO+ fixedEnv =+ M.fromList+ [ ("NIX_LOG_FD", "2"),+ ("TERM", "xterm-256color")+ ]+ onlyImpureOverridableEnv =+ M.fromList+ [ ("NIX_BUILD_TOP", "/build"),+ ("TMPDIR", "/build"),+ ("TEMPDIR", "/build"),+ ("TMP", "/build"),+ ("TEMP", "/build")+ ]+ (//) :: Ord k => Map k a -> Map k a -> Map k a+ (//) = flip M.union+ Container.run+ runcDir+ Container.Config+ { extraBindMounts =+ [ BindMount {pathInContainer = "/build", pathInHost = buildDir, readOnly = False},+ BindMount {pathInContainer = "/etc", pathInHost = etcDir, readOnly = False},+ BindMount {pathInContainer = "/secrets", pathInHost = secretsDir, readOnly = True},+ BindMount {pathInContainer = "/etc/resolv.conf", pathInHost = "/etc/resolv.conf", readOnly = True},+ BindMount {pathInContainer = "/nix/var/nix/daemon-socket/socket", pathInHost = "/nix/var/nix/daemon-socket/socket", readOnly = True}+ ],+ executable = decodeUtf8With lenientDecode drvBuilder,+ arguments = map (decodeUtf8With lenientDecode) drvArgs,+ environment = overridableEnv // drvEnv' // onlyImpureOverridableEnv // impureEnvVars // fixedEnv,+ workingDirectory = "/build",+ hostname = "hercules-ci",+ rootReadOnly = False+ }
+ src/Hercules/Effect/Container.hs view
@@ -0,0 +1,138 @@+{-# LANGUAGE BlockArguments #-}+{-# LANGUAGE NumericUnderscores #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Hercules.Effect.Container where++import Control.Lens+import Data.Aeson (Value (String), eitherDecode, encode, object, toJSON)+import Data.Aeson.Lens+import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy as BL+import qualified Data.Map as M+import qualified Data.UUID.V4 as UUID+import GHC.IO.Exception (IOErrorType (HardwareFault))+import Protolude+import System.Directory (createDirectory)+import System.FilePath ((</>))+import System.IO.Error (ioeGetErrorType)+import System.Posix.IO (closeFd, fdToHandle)+import System.Posix.Terminal (openPseudoTerminal)+import System.Process (CreateProcess (..), StdStream (UseHandle), proc, waitForProcess, withCreateProcess)+import System.Process.ByteString (readCreateProcessWithExitCode)++data BindMount = BindMount+ { pathInContainer :: Text,+ pathInHost :: Text,+ readOnly :: Bool+ }++defaultBindMount :: Text -> BindMount+defaultBindMount path = BindMount {pathInContainer = path, pathInHost = path, readOnly = True}++data Config = Config+ { extraBindMounts :: [BindMount],+ executable :: Text,+ arguments :: [Text],+ environment :: Map Text Text,+ workingDirectory :: Text,+ hostname :: Text,+ rootReadOnly :: Bool+ }++effectToRuncSpec :: Config -> Value -> Value+effectToRuncSpec config spec =+ let defaultMounts = [defaultBindMount "/nix/store"]+ mounts =+ foldMap+ ( \bindMount ->+ pure $+ object+ [ ("destination", String $ pathInContainer bindMount),+ ("source", String $ pathInHost bindMount),+ ("type", "bind"),+ ( "options",+ toJSON $+ ["bind" :: Text]+ <> ["ro" | readOnly bindMount]+ )+ ]+ )+ (defaultMounts <> extraBindMounts config)+ in spec+ & key "process" . key "args" .~ toJSON ([executable config] <> arguments config)+ & key "mounts" . _Array %~ (<> mounts)+ & key "process" . key "terminal" .~ toJSON True+ & key "process" . key "env" .~ toJSON (config & environment & M.toList <&> \(k, v) -> k <> "=" <> v)+ & key "process" . key "cwd" .~ toJSON (config & workingDirectory)+ & key "hostname" .~ toJSON (config & hostname)+ & key "root" . key "readonly" .~ toJSON (config & rootReadOnly)++run :: FilePath -> Config -> IO ExitCode+run dir config = do+ let runcExe = "runc"+ createConfigJsonSpec =+ (System.Process.proc runcExe ["spec", "--rootless"])+ { cwd = Just dir+ }+ configJsonPath = dir </> "config.json"+ (exit, _out, err) <- readCreateProcessWithExitCode createConfigJsonSpec ""+ case exit of+ ExitSuccess -> pass+ ExitFailure e -> do+ putErrText (decodeUtf8With lenientDecode err)+ panic $ "Could not create container configuration template. runc terminated with exit code " <> show e+ templateBytes <- BS.readFile configJsonPath+ template <- case eitherDecode (BL.fromStrict templateBytes) of+ Right a -> pure a+ Left e -> throwIO (FatalError $ "decoding runc config.json template: " <> show e)+ let configJson = effectToRuncSpec config template+ BS.writeFile (dir </> "config.json") (BL.toStrict $ encode configJson)+ createDirectory (dir </> "rootfs")+ name <- do+ uuid <- UUID.nextRandom+ pure $ "hercules-ci-" <> show uuid+ (exitCode, _) <- bracket+ openPseudoTerminal+ ( \(fd1, fd2) -> handle (\(_e :: SomeException) -> pass) do+ closeFd fd1+ when (fd2 /= fd1) (closeFd fd2)+ )+ $ \(master, terminal) -> do+ concurrently+ ( do+ terminalHandle <- fdToHandle terminal+ let createProcSpec =+ (System.Process.proc runcExe ["run", name])+ { std_in = UseHandle terminalHandle, -- can't pass /dev/null :(+ std_out = UseHandle terminalHandle,+ std_err = UseHandle terminalHandle,+ cwd = Just dir+ }+ withCreateProcess createProcSpec \_subStdin _noOut _noErr processHandle -> do+ waitForProcess processHandle+ `onException` ( do+ putErrText "Terminating effect process..."+ _ <- System.Process.withCreateProcess (System.Process.proc runcExe ["kill", name]) \_ _ _ kh ->+ waitForProcess kh+ threadDelay 3_000_000+ _ <- System.Process.withCreateProcess (System.Process.proc runcExe ["kill", name, "KILL"]) \_ _ _ kh ->+ waitForProcess kh+ putErrText "Killed effect process."+ )+ )+ ( do+ masterHandle <- fdToHandle master+ let shovel =+ handleEOF (BS.hGetLine masterHandle) >>= \case+ "" -> pass+ someBytes | "@nix" `BS.isPrefixOf` someBytes -> do+ -- TODO use it (example @nix { "action": "setPhase", "phase": "effectPhase" })+ shovel+ someBytes -> do+ BS.hPut stderr (someBytes <> "\n")+ shovel+ handleEOF = handle \e -> if ioeGetErrorType e == HardwareFault then pure "" else throwIO e+ shovel+ )+ pure exitCode
test/Hercules/Agent/Nix/RetrieveDerivationInfoSpec.hs view
@@ -2,10 +2,10 @@ module Hercules.Agent.Nix.RetrieveDerivationInfoSpec where -import CNix import qualified Data.Map as M import Hercules.API.Agent.Evaluate.EvaluateEvent.DerivationInfo import Hercules.Agent.Nix.RetrieveDerivationInfo+import Hercules.CNix import Protolude import Test.Hspec
test/TestMain.hs view
@@ -1,13 +1,13 @@ module Main where -import CNix+import Hercules.CNix (init) import Protolude import qualified Spec import Test.Hspec.Runner main :: IO () main = do- CNix.init+ init hspecWith config Spec.spec where config = defaultConfig {configColorMode = ColorAlways}