b9-0.5.65: src/lib/B9.hs
{-| B9 is a library and build tool with primitive operations to rmrun a
build script inside a virtual machine and to create and convert
virtual machine image files as well as related ISO and VFAT disk images
for e.g. cloud-init configuration sources.
This module re-exports the modules needed to build a tool around the
library, e.g. see @src\/cli\/Main.hs@ as an example.
"B9.Artifact.Generator" is the module containing the basic data structure
used to describe a B9 build.
-}
module B9
( b9Version
, b9VersionString
, runShowVersion
, runBuildArtifacts
, runFormatBuildFiles
, runPush
, runPull
, runRun
, runGcLocalRepoCache
, runGcRemoteRepoCache
, runListSharedImages
, runAddRepo
, runLookupLocalSharedImage
, module X
) where
import B9.Artifact.Content as X
import B9.Artifact.Content.AST as X
import B9.Artifact.Content.CloudConfigYaml as X
import B9.Artifact.Content.ErlangPropList as X
import B9.Artifact.Content.ErlTerms as X
import B9.Artifact.Content.Readable as X
import B9.Artifact.Content.StringTemplate as X
import B9.Artifact.Content.YamlObject as X
import B9.Artifact.Readable as X
import B9.Artifact.Readable.Interpreter as X
import B9.B9Config as X
import B9.B9Error as X
import B9.B9Exec as X
import B9.B9Logging as X
import B9.B9Monad as X
import B9.BuildInfo as X
import B9.DiskImageBuilder as X
import B9.DiskImages as X
import B9.Environment as X
import B9.ExecEnv as X
import B9.QCUtil as X
import B9.Repository as X
import B9.RepositoryIO as X
import B9.ShellScript as X
import B9.Vm as X
import B9.VmBuilder as X
import Control.Applicative as X
import Control.Exception (catch, throwIO)
import Control.Lens as X (Lens, (%~), (&),
(.~), (^.))
import Control.Monad as X
import Control.Monad.IO.Class as X
import Control.Monad.Reader as X (ReaderT, ask, local)
import Data.Function (on)
import Data.List as X
import Data.Maybe as X
import Data.Monoid as X
import Data.Version as X
import Paths_b9 (version)
import System.Directory (removeFile)
import System.Exit as X (ExitCode (..),
exitWith)
import System.FilePath as X (replaceExtension,
takeDirectory,
takeFileName, (<.>),
(</>))
import System.IO.B9Extras as X
import System.IO.Error (isDoesNotExistError)
import Text.Printf as X (printf)
import Text.Show.Pretty as X (ppShow)
-- | Return the cabal package version of the B9 library.
b9Version :: Version
b9Version = version
-- | Return the cabal package version of the B9 library,
-- formatted using `showVersion`.
b9VersionString :: String
b9VersionString = showVersion version
-- | Just print the 'b9VersionString'
runShowVersion :: MonadIO m => m ()
runShowVersion = liftIO $ putStrLn b9VersionString
-- | Execute the artifact generators defined in a list of text files.
-- Read the text files in the list and parse them as 'ArtifactGenerator's
-- then 'mappend' them and apply 'buildArtifacts' to them.
runBuildArtifacts :: [FilePath] -> B9ConfigAction String
runBuildArtifacts buildFiles = do
generators <- mapM consult buildFiles
runB9 (buildArtifacts (mconcat generators))
-- | Read all text files and parse them as 'ArtifactGenerator's.
-- Then overwrite the files with their contents but _pretty printed_
-- (i.e. formatted).
runFormatBuildFiles :: MonadIO m => [FilePath] -> m ()
runFormatBuildFiles buildFiles =
liftIO $ do
generators <- mapM consult buildFiles
let generatorsFormatted = map ppShow (generators :: [ArtifactGenerator])
putStrLn `mapM` generatorsFormatted
zipWithM_ writeFile buildFiles generatorsFormatted
-- | Upload a 'SharedImageName' to the default remote repository.
-- Note: The remote repository is specified in the 'B9Config'.
runPush :: SharedImageName -> B9ConfigAction ()
runPush name =
localB9Config (keepTempDirs .~ False) $
runB9 $ do
conf <- getConfig
if isNothing (conf ^. repository)
then errorExitL "No repository specified! Use '-r' to specify a repo BEFORE 'push'."
else pushSharedImageLatestVersion name
-- | Either pull a list of available 'SharedImageName's from the remote
-- repository if 'Nothing' is passed as parameter, or pull the latest version
-- of the image from the remote repository. Note: The remote repository is
-- specified in the 'B9Config'.
runPull :: Maybe SharedImageName -> B9ConfigAction ()
runPull mName = localB9Config (keepTempDirs .~ False) (runB9 (pullRemoteRepos >> maybePullImage))
where
maybePullImage = mapM_ (\name -> pullLatestImage name >>= maybe (failPull name) (const (return ()))) mName
failPull name = errorExitL (printf "failed to pull: %s" (show name))
-- | Execute an interactive root shell in a running container from a
-- 'SharedImageName'.
runRun :: SharedImageName -> [String] -> B9ConfigAction String
runRun (SharedImageName name) cmdAndArgs =
localB9Config ((keepTempDirs .~ False) . (interactive .~ True)) (runB9 (buildArtifacts runCmdAndArgs))
where
runCmdAndArgs =
Artifact
(IID ("run-" ++ name))
(VmImages
[ImageTarget Transient (From name KeepSize) (MountPoint "/")]
(VmScript X86_64 [SharedDirectory "." (MountPoint "/mnt/CWD")] (Run (head cmdAndArgs') (tail cmdAndArgs'))))
where
cmdAndArgs' =
if null cmdAndArgs
then ["/usr/bin/zsh"]
else cmdAndArgs
-- | Delete all obsolete versions of all 'SharedImageName's.
runGcLocalRepoCache :: B9ConfigAction ()
runGcLocalRepoCache = localB9Config (keepTempDirs .~ False) (runB9 impl)
where
impl = do
toDelete <- obsoleteSharedmages . map snd <$> lookupSharedImages (== Cache) (const True)
imgDir <- getSharedImagesCacheDir
let filesToDelete = (imgDir </>) <$> (infoFiles ++ imgFiles)
infoFiles = sharedImageFileName <$> toDelete
imgFiles = imageFileName . sharedImageImage <$> toDelete
if null filesToDelete
then liftIO $ putStrLn "\n\nNO IMAGES TO DELETE\n"
else liftIO $ do
putStrLn "DELETING FILES:"
putStrLn (unlines filesToDelete)
mapM_ removeIfExists filesToDelete
where
obsoleteSharedmages :: [SharedImage] -> [SharedImage]
obsoleteSharedmages = concatMap (tail . reverse) . filter ((> 1) . length) . groupBy ((==) `on` sharedImageName)
removeIfExists :: FilePath -> IO ()
removeIfExists fileName = removeFile fileName `catch` handleExists
where
handleExists e
| isDoesNotExistError e = return ()
| otherwise = throwIO e
-- | Clear the shared image cache for a remote. Note: The remote repository is
-- specified in the 'B9Config'.
runGcRemoteRepoCache :: B9ConfigAction ()
runGcRemoteRepoCache =
localB9Config
(keepTempDirs .~ False)
(runB9
(do repos <- getSelectedRepos
cache <- getRepoCache
mapM_ (cleanRemoteRepo cache) repos))
-- | Print a list of shared images cached locally or remotely, if a remote
-- repository was selected. Note: The remote repository is
-- specified in the 'B9Config'.
runListSharedImages :: B9ConfigAction [SharedImage]
runListSharedImages =
localB9Config
(keepTempDirs .~ False)
(runB9
(do MkSelectedRemoteRepo remoteRepo <- getSelectedRemoteRepo
let repoPred = maybe (== Cache) ((==) . toRemoteRepository) remoteRepo
allRepos <- getRemoteRepos
if isNothing remoteRepo
then liftIO $ do
putStrLn "Showing local shared images only.\n"
putStrLn "To view the contents of a remote repo add"
putStrLn "the '-r' switch with one of the remote"
putStrLn "repository ids."
else liftIO $ putStrLn ("Showing shared images on: " ++ remoteRepoRepoId (fromJust remoteRepo))
unless (null allRepos) $
liftIO $ do
putStrLn "\nAvailable remote repositories:"
mapM_ (putStrLn . (" * " ++) . remoteRepoRepoId) allRepos
imgs <- lookupSharedImages repoPred (const True)
if null imgs
then liftIO $ putStrLn "\n\nNO SHARED IMAGES\n"
else liftIO $ do
putStrLn ""
putStrLn $ prettyPrintSharedImages $ map snd imgs
return (map snd imgs)))
-- | Check the SSH settings for a remote repository and add it to the user wide
-- B9 configuration file.
runAddRepo :: RemoteRepo -> B9ConfigAction ()
runAddRepo repo = do
repo' <- remoteRepoCheckSshPrivKey repo
modifyPermanentConfig
(Endo (remoteRepos %~ (mappend [repo'] . filter ((== remoteRepoRepoId repo') . remoteRepoRepoId))))
-- | Find the most recent version of a 'SharedImageName' in the local image cache.
runLookupLocalSharedImage :: SharedImageName -> B9ConfigAction (Maybe SharedImageBuildId)
runLookupLocalSharedImage n =
runB9 $ do
traceL (printf "Searching for cached image: %s" (show n))
imgs <- lookupSharedImages isAvailableOnLocalHost hasTheDesiredName
traceL "Candidate images: "
traceL (printf "%s\n" (prettyPrintSharedImages (map snd imgs)))
let res = extractNewestImageFromResults imgs
traceL (printf "Returning result: %s" (show res))
return res
where
extractNewestImageFromResults = listToMaybe . map toBuildId . take 1 . reverse . map snd
where
toBuildId (SharedImage _ _ i _ _) = i
isAvailableOnLocalHost = (Cache ==)
hasTheDesiredName (SharedImage n' _ _ _ _) = n == n'