podenv-0.2.0: src/Podenv/Build.hs
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE NoImplicitPrelude #-}
-- | This modules contains logic to perform application runtime build
module Podenv.Build
( prepare,
BuildEnv (..),
containerBuildRuntime,
nixRuntime,
)
where
import Control.Monad qualified
import Data.Digest.Pure.SHA qualified as SHA
import Data.Text qualified as Text
import Data.Text.IO qualified as Text
import Podenv.Config qualified
import Podenv.Dhall
import Podenv.Prelude
import Podenv.Runtime (ImageName (..))
import Podenv.Runtime qualified
import System.Directory (doesDirectoryExist, renameFile)
import System.Exit (ExitCode (ExitSuccess))
import System.Process.Typed qualified as P
-- | Helper function to run a standalone app, usefull for local build
type AppRunner = Application -> IO ()
-- | A build env contains action to be performed before preparation and execution
data BuildEnv = BuildEnv
{ beInfos :: Text,
-- | Builds the runtime
beEnsure :: AppRunner -> IO (),
-- | Updates the runtime
beUpdate :: AppRunner -> IO ()
}
defaultBuildEnv :: Text -> BuildEnv
defaultBuildEnv beInfos = BuildEnv {..}
where
beEnsure = const $ pure ()
beUpdate = const $ pure ()
-- | Create the build env
prepare :: Podenv.Runtime.RuntimeEnv -> Application -> IO (BuildEnv, Application)
prepare re app = case runtime app of
Image name -> pure (defaultBuildEnv name, addArgs app)
Container cb -> pure (prepareContainer cb, addArgs app)
Rootfs fp -> pure (defaultBuildEnv fp, addArgs app)
Nix expr -> prepareNix re app expr
where
addArgs = appCommand %~ (<> Podenv.Runtime.extraArgs re)
containerBuildRuntime :: ContainerBuild -> Podenv.Runtime.RuntimeContext
containerBuildRuntime = Podenv.Runtime.Container . mkImageName
mkImageName :: ContainerBuild -> ImageName
mkImageName containerBuild = ImageName $ "localhost/" <> name
where
-- The image name can be set by the container build,
-- otherwise it default to the Containerfile hash
name = fromMaybe imageHash (containerBuild ^. cbImage_name)
imageHash = toText . SHA.showDigest . SHA.sha256 . encodeUtf8 $ containerBuild ^. cbContainerfile
-- | Container build env
prepareContainer :: ContainerBuild -> BuildEnv
prepareContainer containerBuild = BuildEnv {..}
where
-- buildenv basic info:
beInfos = "# Containerfile " <> imageName <> "\n" <> fileContent
fileContent = containerBuild ^. cbContainerfile
ImageName imageName = mkImageName containerBuild
fileName = "Containerfile_" <> toString (imageNameToFP imageName)
where
imageNameToFP = Text.replace "/" "_" . Text.replace ":" "-"
beEnsure = const $ do
imageReady <- checkImageExist imageName
unless imageReady $
buildImage imageName fileName fileContent (containerBuild ^. cbImage_volumes)
beUpdate = const $ case containerBuild ^. cbImage_update of
Nothing -> error "The container is missing the `image_update` attribute"
Just cmd -> do
buildImage
imageName
(fileName <> "-update")
(unlines ["FROM " <> imageName, "RUN " <> cmd])
(containerBuild ^. cbImage_volumes)
-- | Nix runtime re-use the host root filesystem, prepareNix added the nix-store volume.
nixRuntime :: Podenv.Runtime.RuntimeContext
nixRuntime = Podenv.Runtime.Bubblewrap "/"
getCertLocation :: IO (Maybe FilePath)
getCertLocation = runMaybeT $ Control.Monad.msum $ [checkEnv] <> (checkPath <$> paths)
where
checkEnv :: MaybeT IO FilePath
checkEnv = do
env <- lift $ lookupEnv "NIX_SSL_CERT_FILE"
case env of
Just fp -> checkPath fp
Nothing -> mzero
checkPath :: FilePath -> MaybeT IO FilePath
checkPath fp = do
exist <- lift $ doesPathExist fp
unless exist mzero
pure fp
-- Copied from profile.d/nix.sh
paths =
[ "/etc/pki/tls/certs/ca-bundle.crt",
"/etc/ssl/certs/ca-certificates.crt",
"/etc/ssl/ca-bundle.pem",
"/etc/ssl/certs/ca-bundle.crt"
]
-- | Nix build env
prepareNix :: Podenv.Runtime.RuntimeEnv -> Application -> Flakes -> IO (BuildEnv, Application)
prepareNix re app flakes = do
certs <- toText . fromMaybe (error "Can't find ca-bundle") <$> getCertLocation
-- TODO: check howto re-use the host /nix
pure
( BuildEnv
{ beInfos = "# Nix expr:\n" <> Text.unwords nixArgs,
beEnsure = beEnsure certs,
beUpdate = const $ error "Nix update is not implemented"
},
updateApp certs app
)
where
name = app ^. appName
fileName = toString $ "nix_" <> name
-- The location where we expect to find the `nix` command
nixStore = Podenv.Runtime.volumesDir re </> "nix-store"
nixCommandProfile = "var/nix/profiles/nix-install"
nixCommandPath = "/nix/" <> nixCommandProfile <> "/bin/nix"
nixFlags = ["--extra-experimental-features", "nix-command flakes"]
-- The nix command args
nixExtraArgs = case nixpkgs flakes of
Just pin | not (all (Text.isPrefixOf pin) (installables flakes)) -> ["--override-input", "nixpkgs", pin]
_ -> []
nixArgs = nixExtraArgs <> installables flakes
beEnsure certs runApp = do
built <- checkIfBuilt fileName (show nixArgs)
unless built $ do
ensureNixInstalled
_ <- runApp (buildApp certs)
-- save that the build succeeded
cacheDir <- getCacheDir
Text.writeFile (cacheDir </> fileName) (show nixArgs)
debug = when (Podenv.Runtime.verbose re) . hPutStrLn stderr . mappend "[+] "
ensureNixInstalled = do
debug $ "Checking if " <> nixStore </> nixCommandProfile <> " exists"
nixInstalled <- doesSymlinkExist $ nixStore </> nixCommandProfile
unless nixInstalled $ do
debug $ "Checking if " <> nixStore </> "store" <> " exists"
storeExist <- doesDirectoryExist $ nixStore </> "store"
when storeExist $ error $ "existing nix-store is invalid, try removing " <> toText nixStore
podenv <- getExecutablePath
debug $ "[+] Installing nix-store with " <> podenv <> " nix.setup"
P.runProcess_ $ P.setDelegateCtlc True $ P.proc podenv ["nix.setup"]
-- The Application to build the expression, it is executed in advance to separate build and execution
buildApp certs =
Podenv.Config.defaultApp
{ runtime = Rootfs "/",
name = "build-" <> name,
volumes = [toText nixStore <> ":/nix", "nix-setup-home:~/", "nix-cache:~/.cache/nix"],
environ = ["NIX_SSL_CERT_FILE=" <> certs, "LC_ALL=C.UTF-8", "TERM=xterm"],
command =
[toText nixCommandPath, "--verbose"]
<> nixFlags
<> ["build", "--no-link"]
<> nixArgs
}
& (appCapabilities . capNetwork .~ True)
runCommand =
[toText nixCommandPath] <> nixFlags <> case app ^. appCommand of
[] ->
["run"] <> nixArgs <> case Podenv.Runtime.extraArgs re of
[] -> []
xs -> ["--"] <> xs
appArgs -> ["shell"] <> nixArgs <> ["--command"] <> appArgs <> Podenv.Runtime.extraArgs re
addCommand = appCommand .~ runCommand
addVolumes = appVolumes %~ mappend ["nix-store:/nix", "nix-cache:~/.cache/nix", "nix-config:~/.config/nix"]
addEnvirons certs =
appEnviron
%~ mappend
[ "NIX_SSL_CERT_FILE=" <> certs,
"TERM=xterm",
"LC_ALL=C.UTF-8",
"PATH=/nix/var/nix/profiles/nix-install/bin:/bin"
]
updateApp certs = addCommand . addVolumes . addEnvirons certs
-- | Build a container image
buildImage :: Text -> FilePath -> Text -> [Text] -> IO ()
buildImage imageName fileName containerfile volumes = do
uid <- getRealUserID
cacheDir <- getCacheDir
createDirectoryIfMissing True cacheDir
let want = fileName <> ".want"
wantfp = cacheDir </> want
Text.writeFile wantfp containerfile
-- podman build does not support regular volume, lets ensure absolute path
volumesArgs <- traverse (mkVolumeArg cacheDir) volumes
let buildArgs =
["build"]
<> ["-t", toString imageName]
<> ["--build-arg", "USER_UID=" <> show uid]
<> map toString volumesArgs
<> ["-f", want, cacheDir]
cmd = Podenv.Runtime.podman buildArgs
-- putTextLn $ "Building " <> imageName <> " with " <> toText want <> ": " <> show cmd
P.runProcess_ cmd
-- save that the build succeeded
renameFile wantfp (cacheDir </> fileName)
where
mkVolumeArg :: FilePath -> Text -> IO Text
mkVolumeArg cacheDir volume = do
createDirectoryIfMissing True hostPath
pure $ "-v=" <> toText hostPath <> ":" <> containerPath <> ":Z"
where
(p1, p2) = Text.break (== ':') volume
hostPath = cacheDir </> toString p1
containerPath = Text.drop 1 p2
checkImageExist :: Text -> IO Bool
checkImageExist imageName = do
res <- P.runProcess (Podenv.Runtime.podman ["image", "exists", Text.unpack imageName])
pure $ res == ExitSuccess
checkIfBuilt :: FilePath -> Text -> IO Bool
checkIfBuilt filename expected = do
cacheDir <- getCacheDir
current <- readFileM (cacheDir </> filename)
pure $ current == expected