packages feed

cachix-0.3.0: src/Cachix/Client/InstallationMode.hs

{-# LANGUAGE QuasiQuotes #-}

module Cachix.Client.InstallationMode
  ( InstallationMode (..),
    NixEnv (..),
    getInstallationMode,
    addBinaryCache,
    isTrustedUser,
    getUser,
    fromString,
    toString,
    UseOptions (..)
    )
where

import Cachix.Api as Api
import Cachix.Client.Config (Config)
import Cachix.Client.Exception (CachixException (..))
import qualified Cachix.Client.NetRc as NetRc
import qualified Cachix.Client.NixConf as NixConf
import Data.String.Here
import qualified Data.Text as T
import Protolude
import System.Directory (Permissions, createDirectoryIfMissing, getPermissions, writable)
import System.Environment (lookupEnv)
import System.FilePath ((</>), replaceFileName)
import Prelude (String)

data NixEnv
  = NixEnv
      { isTrusted :: Bool,
        isRoot :: Bool,
        isNixOS :: Bool
        }

-- NOTE: update the list of options for --mode argument in OptionsParser.hs
data InstallationMode
  = Install NixConf.NixConfLoc
  | WriteNixOS
  | UntrustedRequiresSudo
  deriving (Show, Eq)

data UseOptions
  = UseOptions
      { useMode :: Maybe InstallationMode,
        useNixOSFolder :: FilePath
        }
  deriving (Show)

fromString :: String -> Maybe InstallationMode
fromString "root-nixconf" = Just $ Install NixConf.Global
fromString "user-nixconf" = Just $ Install NixConf.Local
fromString "nixos" = Just WriteNixOS
fromString "untrusted-requires-sudo" = Just UntrustedRequiresSudo
fromString _ = Nothing

toString :: InstallationMode -> String
toString (Install NixConf.Global) = "root-nixconf"
toString (Install NixConf.Local) = "user-nixconf"
toString WriteNixOS = "nixos"
toString UntrustedRequiresSudo = "untrusted-requires-sudo"

getInstallationMode :: NixEnv -> InstallationMode
getInstallationMode NixEnv {..}
  | isNixOS && isRoot = WriteNixOS
  | not isNixOS && isRoot = Install NixConf.Global
  | isTrusted = Install NixConf.Local
  | otherwise = UntrustedRequiresSudo

-- | Add a Binary cache to nix.conf, print nixos config or fail
addBinaryCache :: Maybe Config -> Api.BinaryCache -> UseOptions -> InstallationMode -> IO ()
addBinaryCache _ _ _ UntrustedRequiresSudo =
  throwIO
    $ MustBeRoot "Run command as root OR execute: $ echo \"trusted-users = root $USER\" | sudo tee -a /etc/nix/nix.conf && sudo pkill nix-daemon"
addBinaryCache maybeConfig bc useOptions WriteNixOS =
  nixosBinaryCache maybeConfig bc useOptions
addBinaryCache maybeConfig bc@Api.BinaryCache {..} _ (Install ncl) = do
  -- TODO: might need locking one day
  gnc <- NixConf.read NixConf.Global
  (input, output) <-
    case ncl of
      NixConf.Global -> return ([gnc], gnc)
      NixConf.Local -> do
        lnc <- NixConf.read NixConf.Local
        return ([gnc, lnc], lnc)
  let nixconf = fromMaybe (NixConf.NixConf []) output
  netrcLocMaybe <- forM (guard $ not isPublic) $ const $ addPrivateBinaryCacheNetRC maybeConfig bc ncl
  let addNetRCLine :: NixConf.NixConf -> NixConf.NixConf
      addNetRCLine = fromMaybe identity $ do
        netrcLoc <- netrcLocMaybe :: Maybe FilePath
        -- We only add the netrc line for local user configs for now.
        -- On NixOS we assume it will be picked up from the default location.
        guard (ncl == NixConf.Local)
        pure (NixConf.setNetRC $ toS netrcLoc)
  NixConf.write ncl $ addNetRCLine $ NixConf.add bc (catMaybes input) nixconf
  filename <- NixConf.getFilename ncl
  putStrLn $ "Configured " <> uri <> " binary cache in " <> toS filename

nixosBinaryCache :: Maybe Config -> Api.BinaryCache -> UseOptions -> IO ()
nixosBinaryCache maybeConfig bc@Api.BinaryCache {..} UseOptions {useNixOSFolder = baseDirectory} = do
  eitherPermissions <- try $ getPermissions (toS toplevel) :: IO (Either SomeException Permissions)
  case eitherPermissions of
    Left _ -> throwIO $ NixOSInstructions noEtcPermissionInstructions
    Right permissions
      | writable permissions -> installFiles
      | otherwise -> throwIO $ NixOSInstructions noEtcPermissionInstructions
  where
    installFiles = do
      createDirectoryIfMissing True $ toS toplevel
      writeFile (toS glueModuleFile) glueModule
      writeFile (toS cacheModuleFile) cacheModule
      unless isPublic $ void $ addPrivateBinaryCacheNetRC maybeConfig bc NixConf.Global
      putText instructions
    configurationNix :: Text
    configurationNix = toS $ toS baseDirectory </> "configuration.nix"
    namespace :: Text
    namespace = "cachix"
    toplevel :: Text
    toplevel = toS $ toS baseDirectory </> toS namespace
    glueModuleFile :: Text
    glueModuleFile = toplevel <> ".nix"
    cacheModuleFile :: Text
    cacheModuleFile = toplevel <> "/" <> toS name <> ".nix"
    noEtcPermissionInstructions :: Text
    noEtcPermissionInstructions =
      [iTrim|
Could not install NixOS configuration to /etc/nixos/ due to lack of write permissions.

Pass `--nixos-folder /etc/mynixos/` as an alternative location with write permissions.
      |]
    instructions :: Text
    instructions =
      [iTrim|
Cachix configuration written to ${glueModuleFile}.
Binary cache ${name} configuration written to ${cacheModuleFile}.

To start using cachix add the following to your ${configurationNix}:

    imports = [ ./cachix.nix ];

Then run:

    $ nixos-rebuild switch
    |]
    glueModule :: Text
    glueModule =
      [i|
# WARN: this file will get overwritten by $ cachix use <name>
{ pkgs, lib, ... }:

let
  folder = ./cachix;
  toImport = name: value: folder + ("/" + name);
  filterCaches = key: value: value == "regular" && lib.hasSuffix ".nix" key;
  imports = lib.mapAttrsToList toImport (lib.filterAttrs filterCaches (builtins.readDir folder));
in {
  inherit imports;
  nix.binaryCaches = ["https://cache.nixos.org/"];
}
    |]
    cacheModule :: Text
    cacheModule =
      [i|
{
  nix = {
    binaryCaches = [
      "${uri}"
    ];
    binaryCachePublicKeys = [
      ${T.intercalate " " (map (\s -> "\"" <> s <> "\"") publicSigningKeys)}
    ];
  };
}
    |]

-- TODO: allow overriding netrc location
addPrivateBinaryCacheNetRC :: Maybe Config -> Api.BinaryCache -> NixConf.NixConfLoc -> IO FilePath
addPrivateBinaryCacheNetRC maybeConfig bc nixconf = do
  filename <- (`replaceFileName` "netrc") <$> NixConf.getFilename nixconf
  case maybeConfig of
    Nothing -> panic "Run $ cachix authtoken <token>"
    Just config -> do
      let netrcfile = fromMaybe filename Nothing -- TODO: get netrc from nixconf
      NetRc.add config [bc] netrcfile
      putErrText $ "Configured private read access credentials in " <> toS filename
      pure filename

isTrustedUser :: [Text] -> IO Bool
isTrustedUser users = do
  user <- getUser
  -- to detect single user installations
  permissions <- getPermissions "/nix/store"
  let isTrusted = writable permissions || user `elem` users
  when (not (null groups) && not isTrusted) $ do
    -- TODO: support Nix group syntax
    putText "Warn: cachix doesn't yet support checking if user is trusted via groups, so it will recommend sudo"
    putStrLn $ "Warn: groups found " <> T.intercalate "," groups
  return isTrusted
  where
    groups = filter (\u -> T.head u == '@') users

getUser :: IO Text
getUser = do
  maybeUser <- lookupEnv "USER"
  case maybeUser of
    Nothing -> throwIO $ UserEnvNotSet "$USER must be set"
    Just user -> return $ toS user