hercules-ci-cli 0.3.0 → 0.3.1
raw patch · 6 files changed
+111/−37 lines, 6 filesPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
API changes (from Hackage documentation)
+ Hercules.CLI.Common: runAuthenticatedOrDummy :: Bool -> RIO (HerculesClientToken, HerculesClientEnv) b -> IO b
+ Hercules.CLI.Credentials: tryReadEffectTokenFromEnv :: MaybeT IO Text
+ Hercules.CLI.Credentials: tryReadEffectTokenFromFile :: MaybeT IO Text
+ Hercules.CLI.Effect: evaluateEffectDerivation :: (Has HerculesClientToken r, Has HerculesClientEnv r) => Ptr EvalState -> Store -> Maybe ProjectPath -> Text -> Text -> RIO r Derivation
+ Hercules.CLI.Effect: getEffectDrv :: Store -> Ptr EvalState -> Maybe ProjectPath -> Text -> Text -> RIO (HerculesClientToken, HerculesClientEnv) Derivation
+ Hercules.CLI.Secret: getSecretsFilePathXdg :: ProjectPath -> IO FilePath
- Hercules.CLI.Credentials: readToken :: Text -> IO Text
+ Hercules.CLI.Credentials: readToken :: IO Text -> IO Text
- Hercules.CLI.Effect: prepareDerivation :: MonadIO m => Store -> StorePath -> m Derivation
+ Hercules.CLI.Effect: prepareDerivation :: MonadIO m => Store -> Derivation -> m ()
Files
- CHANGELOG.md +10/−0
- hercules-ci-cli.cabal +1/−1
- src/Hercules/CLI/Common.hs +14/−3
- src/Hercules/CLI/Credentials.hs +14/−4
- src/Hercules/CLI/Effect.hs +65/−29
- src/Hercules/CLI/Secret.hs +7/−0
CHANGELOG.md view
@@ -5,6 +5,16 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## 0.3.1 - 2022-05-17++### Added++ - `HERCULES_CI_SECRETS_JSON` can now be used to find the secrets+ in an alternate location.++ - Other improvements for running in sandboxed environments, chiefly+ [`hercules-ci-effects effectVMTest`](https://docs.hercules-ci.com/hercules-ci-effects/reference/nix-functions/effectvmtest/)+ ## 0.3.0 - 2022-03-15 ### Added
hercules-ci-cli.cabal view
@@ -1,7 +1,7 @@ cabal-version: 1.12 name: hercules-ci-cli-version: 0.3.0+version: 0.3.1 synopsis: The hci command for working with Hercules CI homepage: https://docs.hercules-ci.com bug-reports: https://github.com/hercules-ci/hercules-ci-agent/issues
src/Hercules/CLI/Common.hs view
@@ -1,11 +1,12 @@ module Hercules.CLI.Common ( runAuthenticated,+ runAuthenticatedOrDummy, exitMsg, ) where import Hercules.CLI.Client-import Hercules.CLI.Credentials (determineDomain, readToken)+import Hercules.CLI.Credentials (determineDomain, readToken, tryReadEffectToken) import Hercules.CLI.Exception (exitMsg) import Protolude import RIO (RIO, runRIO)@@ -14,6 +15,16 @@ runAuthenticated :: RIO (HerculesClientToken, HerculesClientEnv) b -> IO b runAuthenticated m = do clientEnv <- Hercules.CLI.Client.init- domain <- determineDomain- token <- readToken domain+ token <- readToken determineDomain+ runRIO (HerculesClientToken $ Token $ encodeUtf8 token, clientEnv) m++runAuthenticatedOrDummy ::+ -- | Use fake credential if 'False'.+ Bool ->+ RIO (HerculesClientToken, HerculesClientEnv) b ->+ IO b+runAuthenticatedOrDummy True = runAuthenticated+runAuthenticatedOrDummy False = \m -> do+ clientEnv <- Hercules.CLI.Client.init+ token <- fromMaybe "dummy-token" <$> tryReadEffectToken runRIO (HerculesClientToken $ Token $ encodeUtf8 token, clientEnv) m
src/Hercules/CLI/Credentials.hs view
@@ -109,8 +109,18 @@ Nothing -> throwIO NoCredentialException {noCredentialDomain = domain} Just cred -> pure (personalToken cred) +-- | Try to get a token from the local environment.+--+-- 1. HERCULES_CI_API_TOKEN+-- 2. HERCULES_CI_SECRETS_JSON tryReadEffectToken :: IO (Maybe Text)-tryReadEffectToken = runMaybeT do+tryReadEffectToken = runMaybeT $ tryReadEffectTokenFromEnv <|> tryReadEffectTokenFromFile++tryReadEffectTokenFromEnv :: MaybeT IO Text+tryReadEffectTokenFromEnv = T.pack <$> MaybeT (System.Environment.lookupEnv "HERCULES_CI_API_TOKEN")++tryReadEffectTokenFromFile :: MaybeT IO Text+tryReadEffectTokenFromFile = do inEffect <- MaybeT $ System.Environment.lookupEnv "IN_HERCULES_CI_EFFECT" guard $ inEffect == "true" secretsJsonPath <- MaybeT $ System.Environment.lookupEnv "HERCULES_CI_SECRETS_JSON"@@ -123,8 +133,8 @@ Just x -> pure x Nothing -> throwIO $ FatalError $ "HERCULES_CI_SECRETS_JSON, " <> T.pack secretsJsonPath <> " doesn't have key hercules-ci.data.token" -readToken :: Text -> IO Text-readToken domain = do+readToken :: IO Text -> IO Text+readToken getDomain = do tryReadEffectToken >>= \case Just x -> pure x- Nothing -> readPersonalToken domain+ Nothing -> readPersonalToken =<< getDomain
src/Hercules/CLI/Effect.hs view
@@ -1,9 +1,11 @@ {-# LANGUAGE ApplicativeDo #-} {-# LANGUAGE BlockArguments #-} {-# LANGUAGE DataKinds #-}+{-# LANGUAGE ScopedTypeVariables #-} module Hercules.CLI.Effect where +import qualified Data.ByteString as BS import Data.Has (Has) import qualified Data.Text as T import Hercules.API.Id (Id (Id, idUUID))@@ -14,7 +16,7 @@ import qualified Hercules.Agent.NixFile.HerculesCIArgs as HerculesCIArgs import Hercules.Agent.Sensitive (Sensitive (Sensitive)) import Hercules.CLI.Client (HerculesClientEnv, HerculesClientToken, determineDefaultApiBaseUrl, projectsClient, runHerculesClient)-import Hercules.CLI.Common (runAuthenticated)+import Hercules.CLI.Common (runAuthenticatedOrDummy) import Hercules.CLI.Exception (exitMsg) import Hercules.CLI.Git (getAllBranches, getHypotheticalRefs) import qualified Hercules.CLI.Git as Git@@ -23,9 +25,9 @@ import Hercules.CLI.Project (ProjectPath, getProjectIdAndPath, projectOption, projectPathOwner, projectPathProject, projectPathText) import Hercules.CLI.Secret (getSecretsFilePath) import Hercules.CNix (Store)-import Hercules.CNix.Expr (Match (IsAttrs), Value (rtValue), getAttrBool, getDrvFile, match)+import Hercules.CNix.Expr (EvalState, Match (IsAttrs), Value (rtValue), getAttrBool, getDrvFile, match) import qualified Hercules.CNix.Std.Vector as Std.Vector-import Hercules.CNix.Store (Derivation, StorePath, buildPaths, getDerivationInputs, newStorePathWithOutputs)+import Hercules.CNix.Store (Derivation, buildPaths, getDerivationInputs, newStorePathWithOutputs) import qualified Hercules.CNix.Store as CNix import Hercules.Effect (RunEffectParams (..), runEffect) import Hercules.Error (escalate)@@ -52,34 +54,31 @@ projectOptionMaybe <- optional projectOption refMaybe <- asRefOptions requireToken <- Optparse.flag True False (long "no-token" <> help "Don't get an API token. Disallows access to state files, but can run in untrusted environment or unconfigured repo.")- pure $ runAuthenticated do- withAsync (getProjectEffectData projectOptionMaybe requireToken) \projectPathAsync -> do+ pure $ runAuthenticatedOrDummy requireToken do+ let getProjectInfo =+ case projectOptionMaybe of+ Just x+ | not requireToken ->+ pure+ ProjectData+ { pdProjectPath = Just x,+ pdProjectId = Nothing,+ pdToken = Nothing+ }+ _ -> getProjectEffectData projectOptionMaybe requireToken+ withAsync getProjectInfo \projectPathAsync -> do withNix \store evalState -> do ref <- liftIO $ computeRef refMaybe- isDefaultBranch <- liftIO Git.getIsDefault- args <- liftIO $ createHerculesCIArgs (Just ref)- let attrPath = T.split (== '.') attr- nixFile = GitSource.outPath $ HerculesCIArgs.primaryRepo args- uio <- askUnliftIO- valMaybe <- liftIO $ getOnPushOutputValueByPath evalState (toS nixFile) args (resolveInputs uio evalState projectOptionMaybe) (map encodeUtf8 attrPath)- -- valMaybe <- liftIO $ attrByPath evalState rootValue- attrValue <- case valMaybe of- Nothing -> do- exitMsg $ "Could not find an attribute at path " <> show attrPath <> " in " <> nixFile- Just v -> liftIO (match evalState v) >>= escalate- effectAttrs <- case attrValue of- IsAttrs attrs -> pure attrs- _ -> do- exitMsg $ "Attribute is not an Effect at path " <> show attrPath <> " in " <> nixFile+ derivation <- getEffectDrv store evalState projectOptionMaybe ref attr+ isDefaultBranch <-+ if requireToken+ then liftIO Git.getIsDefault+ else pure True - isEffect <- liftIO $ getAttrBool evalState effectAttrs "isEffect" >>= escalate- when (isEffect /= Just True) do- exitMsg $ "Attribute is not an Effect at path " <> show attrPath <> " in " <> nixFile- drvPath <- getDrvFile evalState (rtValue effectAttrs)- derivation <- prepareDerivation store drvPath apiBaseURL <- liftIO determineDefaultApiBaseUrl ProjectData {pdProjectPath = projectPath, pdProjectId = projectId, pdToken = token} <- wait projectPathAsync secretsJson <- liftIO $ traverse getSecretsFilePath projectPath+ logEnv <- liftIO $ initLogEnv mempty "hci" -- withSystemTempDirectory "hci": -- ERRO[0000] container_linux.go:370: starting container process caused: process_linux.go:459: container init caused: rootfs_linux.go:59: mounting "/run/user/1000/hci6017/secrets" to rootfs at "/run/user/1000/hci6017/runc-state/rootfs/secrets" caused: operation not permitted@@ -111,16 +110,53 @@ } throwIO exitCode -prepareDerivation :: MonadIO m => Store -> StorePath -> m Derivation-prepareDerivation store drvPath = do- derivation <- liftIO $ CNix.getDerivation store drvPath+getEffectDrv :: Store -> Ptr EvalState -> Maybe ProjectPath -> Text -> Text -> RIO (HerculesClientToken, HerculesClientEnv) Derivation+getEffectDrv store evalState projectOptionMaybe ref attr = do+ storeDir <- liftIO $ CNix.storeDir store+ derivation <-+ if decodeUtf8With lenientDecode storeDir `T.isPrefixOf` attr+ then liftIO $ do+ -- Support derivation in arbitrary location+ -- Used in hercules-ci-effects test runner+ let path = attr+ contents <- BS.readFile $ toS path+ let stripDrv s = fromMaybe s (T.stripSuffix ".drv" s)+ CNix.getDerivationFromString store (path & T.takeWhileEnd (/= '/') & stripDrv & encodeUtf8) contents+ else evaluateEffectDerivation evalState store projectOptionMaybe ref attr+ prepareDerivation store derivation+ pure derivation++evaluateEffectDerivation :: (Has HerculesClientToken r, Has HerculesClientEnv r) => Ptr EvalState -> Store -> Maybe ProjectPath -> Text -> Text -> RIO r Derivation+evaluateEffectDerivation evalState store projectOptionMaybe ref attr = do+ args <- liftIO $ createHerculesCIArgs (Just ref)+ let attrPath = T.split (== '.') attr+ nixFile = GitSource.outPath $ HerculesCIArgs.primaryRepo args+ uio <- askUnliftIO+ valMaybe <- liftIO $ getOnPushOutputValueByPath evalState (toS nixFile) args (resolveInputs uio evalState projectOptionMaybe) (map encodeUtf8 attrPath)+ -- valMaybe <- liftIO $ attrByPath evalState rootValue+ attrValue <- case valMaybe of+ Nothing -> do+ exitMsg $ "Could not find an attribute at path " <> show attrPath <> " in " <> nixFile+ Just v -> liftIO (match evalState v) >>= escalate+ effectAttrs <- case attrValue of+ IsAttrs attrs -> pure attrs+ _ -> do+ exitMsg $ "Attribute is not an Effect at path " <> show attrPath <> " in " <> nixFile++ isEffect <- liftIO $ getAttrBool evalState effectAttrs "isEffect" >>= escalate+ when (isEffect /= Just True) do+ exitMsg $ "Attribute is not an Effect at path " <> show attrPath <> " in " <> nixFile+ drvPath <- getDrvFile evalState (rtValue effectAttrs)+ liftIO $ CNix.getDerivation store drvPath++prepareDerivation :: MonadIO m => Store -> Derivation -> m ()+prepareDerivation store derivation = do inputs <- liftIO $ getDerivationInputs store derivation storePathsWithOutputs <- liftIO Std.Vector.new liftIO $ for_ inputs \(input, outputs) -> do swo <- newStorePathWithOutputs input outputs Std.Vector.pushBackFP storePathsWithOutputs swo liftIO $ buildPaths store storePathsWithOutputs- pure derivation ciAttributeArgument :: Optparse.Parser Text ciAttributeArgument =
src/Hercules/CLI/Secret.hs view
@@ -16,6 +16,7 @@ import Hercules.UserException (UserException (UserException)) import qualified Options.Applicative as Optparse import Protolude+import System.Environment (lookupEnv) import System.FilePath (takeDirectory, (</>)) import UnliftIO.Directory (XdgDirectory (XdgConfig), createDirectoryIfMissing, doesFileExist, getXdgDirectory) @@ -108,6 +109,12 @@ getSecretsFilePath :: ProjectPath -> IO FilePath getSecretsFilePath projectPath = do+ lookupEnv "HERCULES_CI_SECRETS_JSON" >>= \case+ Nothing -> getSecretsFilePathXdg projectPath+ Just x -> pure x++getSecretsFilePathXdg :: ProjectPath -> IO FilePath+getSecretsFilePathXdg projectPath = do dir <- getXdgDirectory XdgConfig "hercules-ci" let toPathElement = toS . T.map (\case '/' -> '_'; x -> x) pure $ dir </> "secrets" </> toPathElement (projectPathSite projectPath) </> toPathElement (projectPathOwner projectPath) </> "secrets.json"