cachix-1.1: src/Cachix/Deploy/Activate.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE DuplicateRecordFields #-}
module Cachix.Deploy.Activate where
import qualified Cachix.API.WebSocketSubprotocol as WSS
import qualified Cachix.Client.InstallationMode as InstallationMode
import qualified Cachix.Client.NetRc as NetRc
import Cachix.Client.URI (URI)
import qualified Cachix.Client.URI as URI
import qualified Cachix.Deploy.Log as Log
import qualified Cachix.Types.BinaryCache as BinaryCache
import Cachix.Types.Permission (Permission (..))
import qualified Data.Aeson as Aeson
#if MIN_VERSION_aeson(2,0,0)
import qualified Data.Aeson.KeyMap as HM
#else
import qualified Data.HashMap.Strict as HM
#endif
import qualified Data.Conduit.Combinators as Conduit
import qualified Data.Conduit.Process as Conduit
import qualified Data.Vector as Vector
import Protolude hiding (log, toS)
import Protolude.Conv (toS)
import Servant.Auth.Client (Token (..))
import qualified System.Directory as Directory
import System.FilePath ((</>))
import System.IO.Temp (withSystemTempDirectory)
import System.Process
import Prelude (String)
data Status
= Success
| Failure FailureReason
| Rollback FailureReason
deriving (Show, Exception)
data FailureReason
= NetworkTestFailure
| RollbackScriptExitFailure
| RollbackScriptUnexpectedError IOException
| ShellCommandFailure {command :: String, exitCode :: Int}
| UnexpectedError SomeException
deriving (Show)
instance Exception FailureReason where
displayException = \case
NetworkTestFailure -> "Cannot connect back to Cachix Deploy after activating the new deployment"
RollbackScriptExitFailure -> "The rollback script returned a non-zero exit code"
RollbackScriptUnexpectedError e -> "Cannot run rollback script: " <> displayException e
ShellCommandFailure {command, exitCode} ->
toS $
unwords
[ "Failed to run " <> toS command,
show exitCode
]
UnexpectedError e ->
toS $
unwords
[ "The deployment failed with an unexpected error:",
toS (displayException e)
]
downloadStorePaths ::
-- | Logging context
Log.LogStream ->
-- | Deployment details
WSS.DeploymentDetails ->
-- | Binary cache args
[String] ->
IO ()
downloadStorePaths logStream deploymentDetails cacheArgs = do
-- Download the store path from the binary cache
-- TODO: add GC root so it's preserved for the next command
runShell logStream "nix-store" (["-r", toS storePath] <> cacheArgs)
-- Download the rollback script, if provided
for_ (WSS.rollbackScript deploymentDetails) $ \script ->
runShell logStream "nix-store" (["-r", toS script] <> cacheArgs)
where
storePath = WSS.storePath deploymentDetails
type RollbackAction = IO ()
-- | Activate the profile and return a rollback action if available.
activate ::
-- | Logging context
Log.LogStream ->
-- | Profile name
Text ->
-- | Store path to activate
FilePath ->
-- | Returns a rollback action if available
IO (Maybe RollbackAction)
activate logStream profileName storePath = do
(profilePath, activationScripts) <- getActivationScript profileName storePath
previousProfilePath <- toStorePath profilePath
-- Activate the configuration
-- TODO: Check with Domen whether we can exit early here
forM_ activationScripts $ uncurry (runShell logStream)
pure $ Just rollback <*> previousProfilePath
where
toStorePath profilePath = do
profileExists <- Directory.doesPathExist profilePath
if profileExists
then Just <$> Directory.canonicalizePath profilePath
else pure Nothing
-- We can't use '--rollback' because it just selects the next generation
-- down from our deployment, which is not necessarily the generation that
-- was previously active.
rollback = void . activate logStream profileName
type Command = (String, [String])
getActivationScript :: Text -> FilePath -> IO (FilePath, [Command])
getActivationScript profile storePath = do
let checkPath p = Directory.doesPathExist $ toS storePath </> p
isNixOS <- checkPath "nixos-version"
isNixDarwin <- checkPath "darwin-version"
isHomeManager <- checkPath "hm-version"
user <- InstallationMode.getUser
let systemProfileDir = "/nix/var/nix/profiles"
let perUserProfileDir = systemProfileDir </> "per-user" </> toS user
let mkProfilePath profileBaseDir defaultProfile =
profileBaseDir </> if profile == "" then defaultProfile else toS profile
-- Sets the new profile. This is needed for NixOS and nix-darwin. The Home
-- Manager activation script does the profile setting by itself.
-- TODO: document what happens if the wrong user is used for the agent
let setNewProfile profilePath =
("nix-env", ["-p", profilePath, "--set", storePath])
return $ case (isNixOS, isNixDarwin, isHomeManager) of
(True, _, _) ->
let profilePath = mkProfilePath systemProfileDir "system"
in ( profilePath,
[ setNewProfile profilePath,
(toS storePath </> "bin/switch-to-configuration", ["switch"])
]
)
(_, True, _) ->
-- https://github.com/LnL7/nix-darwin/blob/master/pkgs/nix-tools/darwin-rebuild.sh
let profilePath = mkProfilePath systemProfileDir "system-profiles/system"
in ( profilePath,
[ ("mkdir", ["-p", "-m", "0755", "/nix/var/nix/profiles/system-profiles"]),
setNewProfile profilePath,
(toS storePath </> "activate-user", []),
(toS storePath </> "activate", [])
]
)
(_, _, True) ->
( mkProfilePath perUserProfileDir "home-manager",
[(toS storePath </> "activate", [])]
)
(_, _, _) ->
let profilePath = mkProfilePath systemProfileDir "system"
in (profilePath, [setNewProfile profilePath])
-- TODO: send errors as well
-- TODO: fix either/maybe types
getClosureSize :: [String] -> Text -> IO (Either Text (Maybe Int64))
getClosureSize cacheArgs storePath = do
(exitCode, pathInfoJSON, nixError) <- readProcessWithExitCode "nix" (cacheArgs <> ["--extra-experimental-features", "nix-command", "path-info", "-S", "--json", toS storePath]) ""
case exitCode of
ExitFailure _ -> pure $ Left (toS nixError)
ExitSuccess -> pure $ Right $ Aeson.decode (toS pathInfoJSON) >>= extractClosureSize
extractClosureSize :: Aeson.Value -> Maybe Int64
extractClosureSize (Aeson.Array vector) = case Vector.toList vector of
[Aeson.Object obj] -> case HM.lookup "closureSize" obj of
Just (Aeson.Number num) -> Just $ floor num
_ -> Nothing
_ -> Nothing
extractClosureSize _ = Nothing
-- TODO: don't create tmpfile for public caches
withCacheArgs :: URI -> WSS.AgentInformation -> Text -> ([String] -> IO a) -> IO a
withCacheArgs host agentInfo agentToken m =
withSystemTempDirectory "netrc" $ \dir -> do
let filepath = dir </> "netrc"
args <- case WSS.cache agentInfo of
Just cache -> do
let cacheName = WSS.cacheName cache
let cacheURI = URI.appendSubdomain cacheName host
-- TODO: ugh
let bc =
BinaryCache.BinaryCache
{ BinaryCache.name = "",
BinaryCache.uri = URI.serialize cacheURI,
BinaryCache.publicSigningKeys = [],
BinaryCache.isPublic = WSS.isPublic cache,
BinaryCache.githubUsername = "",
BinaryCache.permission = Read,
BinaryCache.preferredCompressionMethod = BinaryCache.XZ
}
NetRc.add (Token (toS agentToken)) [bc] filepath
return $ cachesArgs cache <> ["--option", "netrc-file", filepath]
Nothing ->
return []
m args
where
cachesArgs :: WSS.Cache -> [String]
cachesArgs cache =
let cacheName = WSS.cacheName cache
cacheURI = URI.appendSubdomain cacheName host
hostname = (URI.hostBS . URI.getHostname) cacheURI
officialCache = "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
substituters = ["--option", "extra-substituters", URI.serialize cacheURI]
noNegativeCaching = ["--option", "narinfo-cache-negative-ttl", "0"]
sigs = ["--option", "trusted-public-keys", officialCache <> " " <> toS hostname <> "-1:" <> toS (WSS.publicKey cache)]
in substituters ++ sigs ++ noNegativeCaching
runShell :: Log.LogStream -> FilePath -> [String] -> IO ()
runShell logStream cmd args = runShellWithExitCode logStream cmd args >>= handleError
where
handleError (ExitFailure exitCode) = throwIO $ ShellCommandFailure {command = cmd, exitCode}
handleError ExitSuccess = pure ()
runShellWithExitCode :: Log.LogStream -> FilePath -> [String] -> IO ExitCode
runShellWithExitCode logStream cmd args = do
Log.streamLine logStream $ "$ " <> toS cmd <> " " <> toS (unwords $ fmap toS args)
(exitCode, _, _) <- Conduit.sourceProcessWithStreams (proc cmd args) Conduit.sinkNull logStream logStream
pure exitCode