on decrease-android-validity help sign in
202 lines 8.2 kB history raw
1{-# LANGUAGE FlexibleContexts #-}
2{-# LANGUAGE OverloadedStrings #-}
3{-# LANGUAGE QuasiQuotes #-}
4{-# LANGUAGE ScopedTypeVariables #-}
5module Obelisk.Command.VmBuilder where
7import Control.Applicative (liftA2)
8import Control.Monad (when, unless)
9import Control.Monad.Catch (handle)
10import Control.Monad.IO.Class (liftIO)
11import Data.Monoid ((<>))
12import Data.String (IsString)
13import Data.String.Here.Uninterpolated (hereLit)
14import Data.Text (Text)
15import qualified Data.Text as T
16import qualified Data.Text.IO as T
17import System.Directory (createDirectoryIfMissing)
18import System.Exit (ExitCode(..))
19import System.FilePath ((<.>), (</>))
20import qualified System.Info
21import System.Process (proc)
23import Obelisk.App (MonadObelisk, getObeliskUserStateDir)
24import Obelisk.CliApp
26-- | Generate the `--builders` argument string to enable the VM builder after ensuring it is available.
27getNixBuildersArg :: MonadObelisk m => m String
28getNixBuildersArg = do
29 stateDir <- liftIO getDockerBuilderStateDir
30 let sshIdFile = stateDir </> sshKeyFileName
31 checkForNixDarwin sshIdFile
32 exists <- containerExists stateDir
33 if exists then startContainer else setupNixDocker stateDir
34 pure $ nixBuildersArgString stateDir
36checkForNixDarwin :: MonadObelisk m => String -> m ()
37checkForNixDarwin sshIdFile = do
38 (exitCode, _, _) <- readCreateProcessWithExitCode $ proc "which" ["darwin-rebuild"]
39 unless (exitCode == ExitSuccess) $ failWith $ T.intercalate "\n"
40 [ "Deployments from macOS require nix-darwin to be installed."
41 , "Follow the installation instructions here: https://github.com/LnL7/nix-darwin"
42 , "You'll also need to add the following to your 'configuration.nix' and run 'darwin-rebuild switch':"
43 , ""
44 , T.intercalate "\n" $ map (" " <>)
45 [ "nix.distributedBuilds = true;"
46 , "nix.buildMachines = [{"
47 , " hostName = " <> T.pack (show (containerName :: String)) <> ";"
48 , " sshUser = \"root\";"
49 , " sshKey = " <> T.pack (show sshIdFile) <> ";"
50 , " sshPort = " <> T.pack (show containerSshPort) <> ";"
51 , " systems = [ \"x86_64-linux\" ];"
52 , " maxJobs = 2;"
53 , "}];"
54 ]
55 ]
57-- | String to pass to nix's `--builders` arguments to enable the VM builder.
58nixBuildersArgString :: FilePath -> String
59nixBuildersArgString stateDir = unwords [containerName, "x86_64-linux", stateDir </> sshKeyFileName, "1", "1", "kvm"]
61-- | Name of Docker container used for the VM builder.
62containerName :: IsString str => str
63containerName = "obelisk-docker-nix-builder"
65-- | Check to see if the Docker container exists. This will exit with a helpful message if Docker is not installed.
66containerExists :: MonadObelisk m => FilePath -> m Bool
67containerExists stateDir = handle (\(_ :: IOError) -> failWith needDockerMsg) $ do
68 containerNames <- fmap (map T.strip . T.lines) $
69 readProcessAndLogStderr Error $
70 proc "docker" ["container", "list", "--all", "--format", "{{.Names}}"]
71 let exists = containerName `elem` containerNames
72 when exists $ testLinuxBuild stateDir
73 pure exists
74 where
75 needDockerMsg = T.intercalate "\n"
76 [ "This feature requires that you have Docker installed and the `docker` command available on your PATH."
77 , "Please go https://docs.docker.com/ to install Docker and try this command again."
78 ]
80-- | SSH port on localhost that connects to the container.
81containerSshPort :: Int
82containerSshPort = 2222
84-- | Start the Docker container; assumes it already exists.
85startContainer :: MonadObelisk m => m ()
86startContainer = withSpinner "Starting VM builder" $
87 callProcessAndLogOutput (Debug, Debug) $
88 proc "docker" ["start", containerName]
90-- | Creates the Docker container; assumes it does not exist.
91setupNixDocker :: MonadObelisk m => FilePath -> m ()
92setupNixDocker stateDir = withSpinner ("Creating Docker container named " <> containerName) $ do
93 liftIO $ do
94 createDirectoryIfMissing True stateDir
95 T.writeFile (stateDir </> "Dockerfile") dockerfile
97 -- Create new SSH keys for this container
98 callProcessAndLogOutput (Debug, Error) $
99 proc "rm" ["-f", stateDir </> sshKeyFileName, stateDir </> sshKeyFileName <.> "pub"]
100 callProcessAndLogOutput (Debug, Error) $
101 proc "ssh-keygen" ["-t", "ed25519", "-f", stateDir </> sshKeyFileName, "-P", ""]
103 -- Build the docker container (which uses the SSH keys in the 'ssh' folder)
104 containerId <- fmap T.strip $ readProcessAndLogStderr Error $
105 proc "docker" ["build", stateDir, "--quiet"]
106 callProcessAndLogOutput (Debug, Error) $ proc "docker"
107 [ "run"
108 , "--restart", "always"
109 , "--detach"
110 , "--publish", show containerSshPort <> ":22"
111 , "--name", T.unpack containerName, T.unpack containerId
112 ]
113 exists <- containerExists stateDir
114 unless exists $
115 failWith $ "Expected to see docker container named " <> containerName <> " but it does not exist."
117 testLinuxBuild stateDir
119-- | The instructions for setting up SSH access to the container for the Nix daemon.
120setupInstructions :: FilePath -> Text
121setupInstructions sshIdFile = T.unlines
122 [ "Setting Up Docker Nix Builder"
123 , "-----------------------------"
124 , ""
125 , "We've created a Docker container that can build for Linux. However, the Nix"
126 , "daemon needs to connect to this container as root. Please run the following"
127 , "commands in a root shell (`sudo su -`) to tell SSH how to access the"
128 , "container. Then try your obelisk command again."
129 , ""
130 , "# sudo su -"
131 , "touch ~/.ssh/config"
132 , "cat >> ~/.ssh/config <<CONF"
133 , ""
134 , sshConfigHost sshIdFile
135 , "CONF"
136 , "ssh " <> containerName <> " nix --version # Answer 'yes' if prompted"
137 ]
139-- | SSH configuration for `.ssh/config` to connect to the Docker container.
140sshConfigHost :: FilePath -> Text
141sshConfigHost sshIdFile = T.unlines
142 [ "Host " <> containerName
143 , " User root"
144 , " HostName 127.0.0.1"
145 , " Port " <> T.pack (show containerSshPort)
146 , " IdentityFile " <> T.pack (show sshIdFile) -- TODO: Check to see how weird paths should be encoded here.
147 ]
149-- | User directory where state (namely the SSH keys) is kept for the Docker container.
150getDockerBuilderStateDir :: IO FilePath
151getDockerBuilderStateDir = liftA2 (</>) getObeliskUserStateDir (pure "nix-docker-builder")
153-- | Run a test build to see if a Linux build can finish successfully.
154testLinuxBuild :: MonadObelisk m => FilePath -> m ()
155testLinuxBuild stateDir
156 | System.Info.os == "linux" = failWith "Using the docker builder is not necessary on linux."
157 | otherwise = do
158 (exitCode, _stdout, stderr) <- readCreateProcessWithExitCode $ proc "nix-build"
159 [ "--no-out-link"
160 , "-E", "(import <nixpkgs> { system = \"x86_64-linux\"; }).writeText \"test\" builtins.currentTime"
161 , "--builders", nixBuildersArgString stateDir
162 ]
163 unless (exitCode == ExitSuccess) $ do
164 let sshIdFile = stateDir </> sshKeyFileName
165 putLog Error $ T.pack stderr
166 failWith $ setupInstructions sshIdFile
168-- Copied from https://github.com/LnL7/nix-docker/blob/8dcfb3aff1f87cdafeecb0d27964b27c3fb8b1d2/ssh/Dockerfile
169-- Renamed "insecure_rsa" to 'sshKeyFileName'
170dockerfile :: Text
171dockerfile = [hereLit|
172FROM lnl7/nix:2018-09-21
174RUN nix-env -f '<nixpkgs>' -iA \
175 gnused \
176 openssh \
177 && nix-store --gc
179RUN mkdir -p /etc/ssh \
180 && echo "sshd:x:498:65534::/var/empty:/run/current-system/sw/bin/nologin" >> /etc/passwd \
181 && cp /root/.nix-profile/etc/ssh/sshd_config /etc/ssh \
182 && sed -i '/^PermitRootLogin/d' /etc/ssh/sshd_config \
183 && echo "PermitRootLogin yes" >> /etc/ssh/sshd_config \
184 && ssh-keygen -f /etc/ssh/ssh_host_rsa_key -N "" -t rsa \
185 && ssh-keygen -f /etc/ssh/ssh_host_dsa_key -N "" -t dsa \
186 && echo "export SSL_CERT_FILE=$SSL_CERT_FILE" >> /etc/bashrc \
187 && echo "export PATH=$PATH" >> /etc/bashrc \
188 && echo "export NIX_PATH=$NIX_PATH" >> /etc/bashrc \
189 && echo "source /etc/bashrc" >> /etc/profile
191|] <> T.unlines
192 [ "ADD " <> T.pack sshKeyFileName <> " /root/.ssh/id_rsa"
193 , "ADD " <> T.pack (sshKeyFileName <.> "pub") <> " /root/.ssh/authorized_keys"
194 ] <> [hereLit|
196EXPOSE 22
197CMD ["/nix/store/mydwfxzk8bka4iwjml033dir6gkmqwic-openssh-7.7p1/bin/sshd", "-D", "-e"]
199|]
201sshKeyFileName :: FilePath
202sshKeyFileName = "id_ed25519_obelisk_vm"