PageRenderTime 29ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Stack/Docker.hs

https://gitlab.com/alx741/stack
Haskell | 942 lines | 842 code | 33 blank | 67 comment | 30 complexity | d90667157f4a2ed82cf00573ebbd4ba4 MD5 | raw file
  1. {-# LANGUAGE CPP, ConstraintKinds, DeriveDataTypeable, FlexibleContexts, MultiWayIf, NamedFieldPuns,
  2. OverloadedStrings, PackageImports, RankNTypes, RecordWildCards, ScopedTypeVariables,
  3. TemplateHaskell, TupleSections #-}
  4. -- | Run commands in Docker containers
  5. module Stack.Docker
  6. (cleanup
  7. ,CleanupOpts(..)
  8. ,CleanupAction(..)
  9. ,dockerCleanupCmdName
  10. ,dockerCmdName
  11. ,dockerHelpOptName
  12. ,dockerPullCmdName
  13. ,entrypoint
  14. ,preventInContainer
  15. ,pull
  16. ,reexecWithOptionalContainer
  17. ,reset
  18. ,reExecArgName
  19. ,StackDockerException(..)
  20. ) where
  21. import Control.Applicative
  22. import Control.Concurrent.MVar.Lifted (MVar,modifyMVar_,newMVar)
  23. import Control.Exception.Lifted
  24. import Control.Monad
  25. import Control.Monad.Catch (MonadThrow,throwM,MonadCatch,MonadMask)
  26. import Control.Monad.IO.Class (MonadIO,liftIO)
  27. import Control.Monad.Logger (MonadLogger,logError,logInfo,logWarn)
  28. import Control.Monad.Reader (MonadReader,asks,runReaderT)
  29. import Control.Monad.Writer (execWriter,runWriter,tell)
  30. import Control.Monad.Trans.Control (MonadBaseControl)
  31. import qualified "cryptohash" Crypto.Hash as Hash
  32. import Data.Aeson.Extended (FromJSON(..),(.:),(.:?),(.!=),eitherDecode)
  33. import Data.ByteString.Builder (stringUtf8,charUtf8,toLazyByteString)
  34. import qualified Data.ByteString.Char8 as BS
  35. import qualified Data.ByteString.Lazy.Char8 as LBS
  36. import Data.Char (isSpace,toUpper,isAscii,isDigit)
  37. import Data.Conduit.List (sinkNull)
  38. import Data.List (dropWhileEnd,intercalate,isPrefixOf,isInfixOf,foldl')
  39. import Data.List.Extra (trim)
  40. import Data.Map.Strict (Map)
  41. import qualified Data.Map.Strict as Map
  42. import Data.Maybe
  43. import Data.Ord (Down(..))
  44. import Data.Streaming.Process (ProcessExitedUnsuccessfully(..))
  45. import Data.Text (Text)
  46. import qualified Data.Text as T
  47. import qualified Data.Text.Encoding as T
  48. import Data.Time (UTCTime,LocalTime(..),diffDays,utcToLocalTime,getZonedTime,ZonedTime(..))
  49. import Data.Version (showVersion)
  50. import GHC.Exts (sortWith)
  51. import Network.HTTP.Client.Conduit (HasHttpManager)
  52. import Path
  53. import Path.Extra (toFilePathNoTrailingSep)
  54. import Path.IO hiding (canonicalizePath)
  55. import qualified Paths_stack as Meta
  56. import Prelude -- Fix redundant import warnings
  57. import Stack.Config (getInContainer)
  58. import Stack.Constants
  59. import Stack.Docker.GlobalDB
  60. import Stack.Types.PackageIndex
  61. import Stack.Types.Version
  62. import Stack.Types.Config
  63. import Stack.Types.Docker
  64. import Stack.Types.Internal
  65. import Stack.Setup (ensureDockerStackExe)
  66. import System.Directory (canonicalizePath,getHomeDirectory)
  67. import System.Environment (getEnv,getEnvironment,getProgName,getArgs,getExecutablePath)
  68. import System.Exit (exitSuccess, exitWith)
  69. import qualified System.FilePath as FP
  70. import System.IO (stderr,stdin,stdout,hIsTerminalDevice)
  71. import System.IO.Error (isDoesNotExistError)
  72. import System.IO.Unsafe (unsafePerformIO)
  73. import qualified System.PosixCompat.User as User
  74. import qualified System.PosixCompat.Files as Files
  75. import System.Process.PagerEditor (editByteString)
  76. import System.Process.Read
  77. import System.Process.Run
  78. import System.Process (CreateProcess(delegate_ctlc))
  79. import Text.Printf (printf)
  80. #ifndef WINDOWS
  81. import Control.Concurrent (threadDelay)
  82. import Control.Monad.Trans.Control (liftBaseWith)
  83. import System.Posix.Signals
  84. import qualified System.Posix.User as PosixUser
  85. #endif
  86. -- | If Docker is enabled, re-runs the currently running OS command in a Docker container.
  87. -- Otherwise, runs the inner action.
  88. --
  89. -- This takes an optional release action which should be taken IFF control is
  90. -- transfering away from the current process to the intra-container one. The main use
  91. -- for this is releasing a lock. After launching reexecution, the host process becomes
  92. -- nothing but an manager for the call into docker and thus may not hold the lock.
  93. reexecWithOptionalContainer
  94. :: M env m
  95. => Maybe (Path Abs Dir)
  96. -> Maybe (m ())
  97. -> IO ()
  98. -> Maybe (m ())
  99. -> Maybe (m ())
  100. -> m ()
  101. reexecWithOptionalContainer mprojectRoot =
  102. execWithOptionalContainer mprojectRoot getCmdArgs
  103. where
  104. getCmdArgs docker envOverride imageInfo isRemoteDocker = do
  105. config <- asks getConfig
  106. deUser <-
  107. if fromMaybe (not isRemoteDocker) (dockerSetUser docker)
  108. then liftIO $ do
  109. duUid <- User.getEffectiveUserID
  110. duGid <- User.getEffectiveGroupID
  111. duGroups <- User.getGroups
  112. duUmask <- Files.setFileCreationMask 0o022
  113. -- Only way to get old umask seems to be to change it, so set it back afterward
  114. _ <- Files.setFileCreationMask duUmask
  115. return (Just DockerUser{..})
  116. else return Nothing
  117. args <-
  118. fmap
  119. (["--" ++ reExecArgName ++ "=" ++ showVersion Meta.version
  120. ,"--" ++ dockerEntrypointArgName
  121. ,show DockerEntrypoint{..}] ++)
  122. (liftIO getArgs)
  123. case dockerStackExe (configDocker config) of
  124. Just DockerStackExeHost
  125. | configPlatform config == dockerContainerPlatform -> do
  126. exePath <- liftIO getExecutablePath
  127. cmdArgs args exePath
  128. | otherwise -> throwM UnsupportedStackExeHostPlatformException
  129. Just DockerStackExeImage -> do
  130. progName <- liftIO getProgName
  131. return (FP.takeBaseName progName, args, [], [])
  132. Just (DockerStackExePath path) -> do
  133. exePath <- liftIO $ canonicalizePath (toFilePath path)
  134. cmdArgs args exePath
  135. Just DockerStackExeDownload -> exeDownload args
  136. Nothing
  137. | configPlatform config == dockerContainerPlatform -> do
  138. (exePath,exeTimestamp,misCompatible) <-
  139. liftIO $
  140. do exePath <- liftIO getExecutablePath
  141. exeTimestamp <- resolveFile' exePath >>= getModificationTime
  142. isKnown <-
  143. liftIO $
  144. getDockerImageExe
  145. config
  146. (iiId imageInfo)
  147. exePath
  148. exeTimestamp
  149. return (exePath, exeTimestamp, isKnown)
  150. case misCompatible of
  151. Just True -> cmdArgs args exePath
  152. Just False -> exeDownload args
  153. Nothing -> do
  154. e <-
  155. try $
  156. sinkProcessStderrStdout
  157. Nothing
  158. envOverride
  159. "docker"
  160. [ "run"
  161. , "-v"
  162. , exePath ++ ":" ++ "/tmp/stack"
  163. , iiId imageInfo
  164. , "/tmp/stack"
  165. , "--version"]
  166. sinkNull
  167. sinkNull
  168. let compatible =
  169. case e of
  170. Left (ProcessExitedUnsuccessfully _ _) ->
  171. False
  172. Right _ -> True
  173. liftIO $
  174. setDockerImageExe
  175. config
  176. (iiId imageInfo)
  177. exePath
  178. exeTimestamp
  179. compatible
  180. if compatible
  181. then cmdArgs args exePath
  182. else exeDownload args
  183. Nothing -> exeDownload args
  184. exeDownload args = do
  185. exePath <- ensureDockerStackExe dockerContainerPlatform
  186. cmdArgs args (toFilePath exePath)
  187. cmdArgs args exePath = do
  188. let mountPath = hostBinDir FP.</> FP.takeBaseName exePath
  189. return (mountPath, args, [], [Mount exePath mountPath])
  190. -- | If Docker is enabled, re-runs the OS command returned by the second argument in a
  191. -- Docker container. Otherwise, runs the inner action.
  192. --
  193. -- This takes an optional release action just like `reexecWithOptionalContainer`.
  194. execWithOptionalContainer
  195. :: M env m
  196. => Maybe (Path Abs Dir)
  197. -> GetCmdArgs env m
  198. -> Maybe (m ())
  199. -> IO ()
  200. -> Maybe (m ())
  201. -> Maybe (m ())
  202. -> m ()
  203. execWithOptionalContainer mprojectRoot getCmdArgs mbefore inner mafter mrelease =
  204. do config <- asks getConfig
  205. inContainer <- getInContainer
  206. isReExec <- asks getReExec
  207. if | inContainer && not isReExec && (isJust mbefore || isJust mafter) ->
  208. throwM OnlyOnHostException
  209. | inContainer ->
  210. liftIO (do inner
  211. exitSuccess)
  212. | not (dockerEnable (configDocker config)) ->
  213. do fromMaybeAction mbefore
  214. liftIO inner
  215. fromMaybeAction mafter
  216. liftIO exitSuccess
  217. | otherwise ->
  218. do fromMaybeAction mrelease
  219. runContainerAndExit
  220. getCmdArgs
  221. mprojectRoot
  222. (fromMaybeAction mbefore)
  223. (fromMaybeAction mafter)
  224. where
  225. fromMaybeAction Nothing = return ()
  226. fromMaybeAction (Just hook) = hook
  227. -- | Error if running in a container.
  228. preventInContainer :: (MonadIO m,MonadThrow m) => m () -> m ()
  229. preventInContainer inner =
  230. do inContainer <- getInContainer
  231. if inContainer
  232. then throwM OnlyOnHostException
  233. else inner
  234. -- | Run a command in a new Docker container, then exit the process.
  235. runContainerAndExit :: M env m
  236. => GetCmdArgs env m
  237. -> Maybe (Path Abs Dir) -- ^ Project root (maybe)
  238. -> m () -- ^ Action to run before
  239. -> m () -- ^ Action to run after
  240. -> m ()
  241. runContainerAndExit getCmdArgs
  242. mprojectRoot
  243. before
  244. after =
  245. do config <- asks getConfig
  246. let docker = configDocker config
  247. envOverride <- getEnvOverride (configPlatform config)
  248. checkDockerVersion envOverride docker
  249. (env,isStdinTerminal,isStderrTerminal,homeDir) <- liftIO $
  250. (,,,)
  251. <$> getEnvironment
  252. <*> hIsTerminalDevice stdin
  253. <*> hIsTerminalDevice stderr
  254. <*> (parseAbsDir =<< getHomeDirectory)
  255. isStdoutTerminal <- asks getTerminal
  256. let dockerHost = lookup "DOCKER_HOST" env
  257. dockerCertPath = lookup "DOCKER_CERT_PATH" env
  258. bamboo = lookup "bamboo_buildKey" env
  259. jenkins = lookup "JENKINS_HOME" env
  260. msshAuthSock = lookup "SSH_AUTH_SOCK" env
  261. muserEnv = lookup "USER" env
  262. isRemoteDocker = maybe False (isPrefixOf "tcp://") dockerHost
  263. image = dockerImage docker
  264. when (isRemoteDocker &&
  265. maybe False (isInfixOf "boot2docker") dockerCertPath)
  266. ($logWarn "Warning: Using boot2docker is NOT supported, and not likely to perform well.")
  267. maybeImageInfo <- inspect envOverride image
  268. imageInfo@Inspect{..} <- case maybeImageInfo of
  269. Just ii -> return ii
  270. Nothing
  271. | dockerAutoPull docker ->
  272. do pullImage envOverride docker image
  273. mii2 <- inspect envOverride image
  274. case mii2 of
  275. Just ii2 -> return ii2
  276. Nothing -> throwM (InspectFailedException image)
  277. | otherwise -> throwM (NotPulledException image)
  278. sandboxDir <- projectDockerSandboxDir projectRoot
  279. let ImageConfig {..} = iiConfig
  280. imageEnvVars = map (break (== '=')) icEnv
  281. platformVariant = BS.unpack $ Hash.digestToHexByteString $ hashRepoName image
  282. stackRoot = configStackRoot config
  283. sandboxHomeDir = sandboxDir </> homeDirName
  284. isTerm = not (dockerDetach docker) &&
  285. isStdinTerminal &&
  286. isStdoutTerminal &&
  287. isStderrTerminal
  288. keepStdinOpen = not (dockerDetach docker) &&
  289. -- Workaround for https://github.com/docker/docker/issues/12319
  290. -- This is fixed in Docker 1.9.1, but will leave the workaround
  291. -- in place for now, for users who haven't upgraded yet.
  292. (isTerm || (isNothing bamboo && isNothing jenkins))
  293. hostBinDirPath <- parseAbsDir hostBinDir
  294. newPathEnv <- augmentPath
  295. [ hostBinDirPath
  296. , sandboxHomeDir </> $(mkRelDir ".local/bin")]
  297. (T.pack <$> lookupImageEnv "PATH" imageEnvVars)
  298. (cmnd,args,envVars,extraMount) <- getCmdArgs docker envOverride imageInfo isRemoteDocker
  299. pwd <- getCurrentDir
  300. liftIO
  301. (do updateDockerImageLastUsed config iiId (toFilePath projectRoot)
  302. mapM_ (ensureDir) [sandboxHomeDir, stackRoot])
  303. -- Since $HOME is now mounted in the same place in the container we can
  304. -- just symlink $HOME/.ssh to the right place for the stack docker user
  305. let sshDir = homeDir </> sshRelDir
  306. sshDirExists <- doesDirExist sshDir
  307. sshSandboxDirExists <-
  308. liftIO
  309. (Files.fileExist
  310. (toFilePathNoTrailingSep (sandboxHomeDir </> sshRelDir)))
  311. when (sshDirExists && not sshSandboxDirExists)
  312. (liftIO
  313. (Files.createSymbolicLink
  314. (toFilePathNoTrailingSep sshDir)
  315. (toFilePathNoTrailingSep (sandboxHomeDir </> sshRelDir))))
  316. containerID <- (trim . decodeUtf8) <$> readDockerProcess
  317. envOverride
  318. (concat
  319. [["create"
  320. ,"--net=host"
  321. ,"-e",inContainerEnvVar ++ "=1"
  322. ,"-e",stackRootEnvVar ++ "=" ++ toFilePathNoTrailingSep stackRoot
  323. ,"-e",platformVariantEnvVar ++ "=dk" ++ platformVariant
  324. ,"-e","HOME=" ++ toFilePathNoTrailingSep sandboxHomeDir
  325. ,"-e","PATH=" ++ T.unpack newPathEnv
  326. ,"-e","PWD=" ++ toFilePathNoTrailingSep pwd
  327. ,"-v",toFilePathNoTrailingSep homeDir ++ ":" ++ toFilePathNoTrailingSep homeDir
  328. ,"-v",toFilePathNoTrailingSep stackRoot ++ ":" ++ toFilePathNoTrailingSep stackRoot
  329. ,"-v",toFilePathNoTrailingSep projectRoot ++ ":" ++ toFilePathNoTrailingSep projectRoot
  330. ,"-v",toFilePathNoTrailingSep sandboxHomeDir ++ ":" ++ toFilePathNoTrailingSep sandboxHomeDir
  331. ,"-w",toFilePathNoTrailingSep pwd]
  332. ,case muserEnv of
  333. Nothing -> []
  334. Just userEnv -> ["-e","USER=" ++ userEnv]
  335. ,case msshAuthSock of
  336. Nothing -> []
  337. Just sshAuthSock ->
  338. ["-e","SSH_AUTH_SOCK=" ++ sshAuthSock
  339. ,"-v",sshAuthSock ++ ":" ++ sshAuthSock]
  340. -- Disable the deprecated entrypoint in FP Complete-generated images
  341. ,["--entrypoint=/usr/bin/env"
  342. | isJust (lookupImageEnv oldSandboxIdEnvVar imageEnvVars) &&
  343. (icEntrypoint == ["/usr/local/sbin/docker-entrypoint"] ||
  344. icEntrypoint == ["/root/entrypoint.sh"])]
  345. ,concatMap (\(k,v) -> ["-e", k ++ "=" ++ v]) envVars
  346. ,concatMap mountArg (extraMount ++ dockerMount docker)
  347. ,concatMap (\nv -> ["-e", nv]) (dockerEnv docker)
  348. ,case dockerContainerName docker of
  349. Just name -> ["--name=" ++ name]
  350. Nothing -> []
  351. ,["-t" | isTerm]
  352. ,["-i" | keepStdinOpen]
  353. ,dockerRunArgs docker
  354. ,[image]
  355. ,[cmnd]
  356. ,args])
  357. before
  358. #ifndef WINDOWS
  359. runInBase <- liftBaseWith $ \run -> return (void . run)
  360. oldHandlers <- forM [sigINT,sigABRT,sigHUP,sigPIPE,sigTERM,sigUSR1,sigUSR2] $ \sig -> do
  361. let sigHandler = runInBase $ do
  362. readProcessNull Nothing envOverride "docker"
  363. ["kill","--signal=" ++ show sig,containerID]
  364. when (sig `elem` [sigTERM,sigABRT]) $ do
  365. -- Give the container 30 seconds to exit gracefully, then send a sigKILL to force it
  366. liftIO $ threadDelay 30000000
  367. readProcessNull Nothing envOverride "docker" ["kill",containerID]
  368. oldHandler <- liftIO $ installHandler sig (Catch sigHandler) Nothing
  369. return (sig, oldHandler)
  370. #endif
  371. let cmd = Cmd Nothing
  372. "docker"
  373. envOverride
  374. (concat [["start"]
  375. ,["-a" | not (dockerDetach docker)]
  376. ,["-i" | keepStdinOpen]
  377. ,[containerID]])
  378. e <- finally
  379. (try $ callProcess'
  380. (\cp -> cp { delegate_ctlc = False })
  381. cmd)
  382. (do unless (dockerPersist docker || dockerDetach docker) $
  383. catch
  384. (readProcessNull Nothing envOverride "docker" ["rm","-f",containerID])
  385. (\(_::ReadProcessException) -> return ())
  386. #ifndef WINDOWS
  387. forM_ oldHandlers $ \(sig,oldHandler) ->
  388. liftIO $ installHandler sig oldHandler Nothing
  389. #endif
  390. )
  391. case e of
  392. Left (ProcessExitedUnsuccessfully _ ec) -> liftIO (exitWith ec)
  393. Right () -> do after
  394. liftIO exitSuccess
  395. where
  396. -- This is using a hash of the Docker repository (without tag or digest) to ensure
  397. -- binaries/libraries aren't shared between Docker and host (or incompatible Docker images)
  398. hashRepoName :: String -> Hash.Digest Hash.MD5
  399. hashRepoName = Hash.hash . BS.pack . takeWhile (\c -> c /= ':' && c /= '@')
  400. lookupImageEnv name vars =
  401. case lookup name vars of
  402. Just ('=':val) -> Just val
  403. _ -> Nothing
  404. mountArg (Mount host container) = ["-v",host ++ ":" ++ container]
  405. projectRoot = fromMaybeProjectRoot mprojectRoot
  406. sshRelDir = $(mkRelDir ".ssh/")
  407. -- | Clean-up old docker images and containers.
  408. cleanup :: M env m
  409. => CleanupOpts -> m ()
  410. cleanup opts =
  411. do config <- asks getConfig
  412. let docker = configDocker config
  413. envOverride <- getEnvOverride (configPlatform config)
  414. checkDockerVersion envOverride docker
  415. let runDocker = readDockerProcess envOverride
  416. imagesOut <- runDocker ["images","--no-trunc","-f","dangling=false"]
  417. danglingImagesOut <- runDocker ["images","--no-trunc","-f","dangling=true"]
  418. runningContainersOut <- runDocker ["ps","-a","--no-trunc","-f","status=running"]
  419. restartingContainersOut <- runDocker ["ps","-a","--no-trunc","-f","status=restarting"]
  420. exitedContainersOut <- runDocker ["ps","-a","--no-trunc","-f","status=exited"]
  421. pausedContainersOut <- runDocker ["ps","-a","--no-trunc","-f","status=paused"]
  422. let imageRepos = parseImagesOut imagesOut
  423. danglingImageHashes = Map.keys (parseImagesOut danglingImagesOut)
  424. runningContainers = parseContainersOut runningContainersOut ++
  425. parseContainersOut restartingContainersOut
  426. stoppedContainers = parseContainersOut exitedContainersOut ++
  427. parseContainersOut pausedContainersOut
  428. inspectMap <- inspects envOverride
  429. (Map.keys imageRepos ++
  430. danglingImageHashes ++
  431. map fst stoppedContainers ++
  432. map fst runningContainers)
  433. (imagesLastUsed,curTime) <-
  434. liftIO ((,) <$> getDockerImagesLastUsed config
  435. <*> getZonedTime)
  436. let planWriter = buildPlan curTime
  437. imagesLastUsed
  438. imageRepos
  439. danglingImageHashes
  440. stoppedContainers
  441. runningContainers
  442. inspectMap
  443. plan = toLazyByteString (execWriter planWriter)
  444. plan' <- case dcAction opts of
  445. CleanupInteractive ->
  446. liftIO (editByteString (intercalate "-" [stackProgName
  447. ,dockerCmdName
  448. ,dockerCleanupCmdName
  449. ,"plan"])
  450. plan)
  451. CleanupImmediate -> return plan
  452. CleanupDryRun -> do liftIO (LBS.hPut stdout plan)
  453. return LBS.empty
  454. mapM_ (performPlanLine envOverride)
  455. (reverse (filter filterPlanLine (lines (LBS.unpack plan'))))
  456. allImageHashesOut <- runDocker ["images","-aq","--no-trunc"]
  457. liftIO (pruneDockerImagesLastUsed config (lines (decodeUtf8 allImageHashesOut)))
  458. where
  459. filterPlanLine line =
  460. case line of
  461. c:_ | isSpace c -> False
  462. _ -> True
  463. performPlanLine envOverride line =
  464. case filter (not . null) (words (takeWhile (/= '#') line)) of
  465. [] -> return ()
  466. (c:_):t:v:_ ->
  467. do args <- if | toUpper c == 'R' && t == imageStr ->
  468. do $logInfo (concatT ["Removing image: '",v,"'"])
  469. return ["rmi",v]
  470. | toUpper c == 'R' && t == containerStr ->
  471. do $logInfo (concatT ["Removing container: '",v,"'"])
  472. return ["rm","-f",v]
  473. | otherwise -> throwM (InvalidCleanupCommandException line)
  474. e <- try (readDockerProcess envOverride args)
  475. case e of
  476. Left ex@ReadProcessException{} ->
  477. $logError (concatT ["Could not remove: '",v,"': ", show ex])
  478. Left e' -> throwM e'
  479. Right _ -> return ()
  480. _ -> throwM (InvalidCleanupCommandException line)
  481. parseImagesOut = Map.fromListWith (++) . map parseImageRepo . drop 1 . lines . decodeUtf8
  482. where parseImageRepo :: String -> (String, [String])
  483. parseImageRepo line =
  484. case words line of
  485. repo:tag:hash:_
  486. | repo == "<none>" -> (hash,[])
  487. | tag == "<none>" -> (hash,[repo])
  488. | otherwise -> (hash,[repo ++ ":" ++ tag])
  489. _ -> throw (InvalidImagesOutputException line)
  490. parseContainersOut = map parseContainer . drop 1 . lines . decodeUtf8
  491. where parseContainer line =
  492. case words line of
  493. hash:image:rest -> (hash,(image,last rest))
  494. _ -> throw (InvalidPSOutputException line)
  495. buildPlan curTime
  496. imagesLastUsed
  497. imageRepos
  498. danglingImageHashes
  499. stoppedContainers
  500. runningContainers
  501. inspectMap =
  502. do case dcAction opts of
  503. CleanupInteractive ->
  504. do buildStrLn
  505. (concat
  506. ["# STACK DOCKER CLEANUP PLAN"
  507. ,"\n#"
  508. ,"\n# When you leave the editor, the lines in this plan will be processed."
  509. ,"\n#"
  510. ,"\n# Lines that begin with 'R' denote an image or container that will be."
  511. ,"\n# removed. You may change the first character to/from 'R' to remove/keep"
  512. ,"\n# and image or container that would otherwise be kept/removed."
  513. ,"\n#"
  514. ,"\n# To cancel the cleanup, delete all lines in this file."
  515. ,"\n#"
  516. ,"\n# By default, the following images/containers will be removed:"
  517. ,"\n#"])
  518. buildDefault dcRemoveKnownImagesLastUsedDaysAgo "Known images last used"
  519. buildDefault dcRemoveUnknownImagesCreatedDaysAgo "Unknown images created"
  520. buildDefault dcRemoveDanglingImagesCreatedDaysAgo "Dangling images created"
  521. buildDefault dcRemoveStoppedContainersCreatedDaysAgo "Stopped containers created"
  522. buildDefault dcRemoveRunningContainersCreatedDaysAgo "Running containers created"
  523. buildStrLn
  524. (concat
  525. ["#"
  526. ,"\n# The default plan can be adjusted using command-line arguments."
  527. ,"\n# Run '" ++ unwords [stackProgName, dockerCmdName, dockerCleanupCmdName] ++
  528. " --help' for details."
  529. ,"\n#"])
  530. _ -> buildStrLn
  531. (unlines
  532. ["# Lines that begin with 'R' denote an image or container that will be."
  533. ,"# removed."])
  534. buildSection "KNOWN IMAGES (pulled/used by stack)"
  535. imagesLastUsed
  536. buildKnownImage
  537. buildSection "UNKNOWN IMAGES (not managed by stack)"
  538. (sortCreated (Map.toList (foldl' (\m (h,_) -> Map.delete h m)
  539. imageRepos
  540. imagesLastUsed)))
  541. buildUnknownImage
  542. buildSection "DANGLING IMAGES (no named references and not depended on by other images)"
  543. (sortCreated (map (,()) danglingImageHashes))
  544. buildDanglingImage
  545. buildSection "STOPPED CONTAINERS"
  546. (sortCreated stoppedContainers)
  547. (buildContainer (dcRemoveStoppedContainersCreatedDaysAgo opts))
  548. buildSection "RUNNING CONTAINERS"
  549. (sortCreated runningContainers)
  550. (buildContainer (dcRemoveRunningContainersCreatedDaysAgo opts))
  551. where
  552. buildDefault accessor description =
  553. case accessor opts of
  554. Just days -> buildStrLn ("# - " ++ description ++ " at least " ++ showDays days ++ ".")
  555. Nothing -> return ()
  556. sortCreated =
  557. sortWith (\(_,_,x) -> Down x) .
  558. mapMaybe (\(h,r) ->
  559. case Map.lookup h inspectMap of
  560. Nothing -> Nothing
  561. Just ii -> Just (h,r,iiCreated ii))
  562. buildSection sectionHead items itemBuilder =
  563. do let (anyWrote,b) = runWriter (forM items itemBuilder)
  564. when (or anyWrote) $
  565. do buildSectionHead sectionHead
  566. tell b
  567. buildKnownImage (imageHash,lastUsedProjects) =
  568. case Map.lookup imageHash imageRepos of
  569. Just repos@(_:_) ->
  570. do case lastUsedProjects of
  571. (l,_):_ -> forM_ repos (buildImageTime (dcRemoveKnownImagesLastUsedDaysAgo opts) l)
  572. _ -> forM_ repos buildKeepImage
  573. forM_ lastUsedProjects buildProject
  574. buildInspect imageHash
  575. return True
  576. _ -> return False
  577. buildUnknownImage (hash, repos, created) =
  578. case repos of
  579. [] -> return False
  580. _ -> do forM_ repos (buildImageTime (dcRemoveUnknownImagesCreatedDaysAgo opts) created)
  581. buildInspect hash
  582. return True
  583. buildDanglingImage (hash, (), created) =
  584. do buildImageTime (dcRemoveDanglingImagesCreatedDaysAgo opts) created hash
  585. buildInspect hash
  586. return True
  587. buildContainer removeAge (hash,(image,name),created) =
  588. do let disp = name ++ " (image: " ++ image ++ ")"
  589. buildTime containerStr removeAge created disp
  590. buildInspect hash
  591. return True
  592. buildProject (lastUsedTime, projectPath) =
  593. buildInfo ("Last used " ++
  594. showDaysAgo lastUsedTime ++
  595. " in " ++
  596. projectPath)
  597. buildInspect hash =
  598. case Map.lookup hash inspectMap of
  599. Just Inspect{iiCreated,iiVirtualSize} ->
  600. buildInfo ("Created " ++
  601. showDaysAgo iiCreated ++
  602. maybe ""
  603. (\s -> " (size: " ++
  604. printf "%g" (fromIntegral s / 1024.0 / 1024.0 :: Float) ++
  605. "M)")
  606. iiVirtualSize)
  607. Nothing -> return ()
  608. showDays days =
  609. case days of
  610. 0 -> "today"
  611. 1 -> "yesterday"
  612. n -> show n ++ " days ago"
  613. showDaysAgo oldTime = showDays (daysAgo oldTime)
  614. daysAgo oldTime =
  615. let ZonedTime (LocalTime today _) zone = curTime
  616. LocalTime oldDay _ = utcToLocalTime zone oldTime
  617. in diffDays today oldDay
  618. buildImageTime = buildTime imageStr
  619. buildTime t removeAge time disp =
  620. case removeAge of
  621. Just d | daysAgo time >= d -> buildStrLn ("R " ++ t ++ " " ++ disp)
  622. _ -> buildKeep t disp
  623. buildKeep t d = buildStrLn (" " ++ t ++ " " ++ d)
  624. buildKeepImage = buildKeep imageStr
  625. buildSectionHead s = buildStrLn ("\n#\n# " ++ s ++ "\n#\n")
  626. buildInfo = buildStrLn . (" # " ++)
  627. buildStrLn l = do buildStr l
  628. tell (charUtf8 '\n')
  629. buildStr = tell . stringUtf8
  630. imageStr = "image"
  631. containerStr = "container"
  632. -- | Inspect Docker image or container.
  633. inspect :: (MonadIO m,MonadLogger m,MonadBaseControl IO m,MonadCatch m)
  634. => EnvOverride -> String -> m (Maybe Inspect)
  635. inspect envOverride image =
  636. do results <- inspects envOverride [image]
  637. case Map.toList results of
  638. [] -> return Nothing
  639. [(_,i)] -> return (Just i)
  640. _ -> throwM (InvalidInspectOutputException "expect a single result")
  641. -- | Inspect multiple Docker images and/or containers.
  642. inspects :: (MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadCatch m)
  643. => EnvOverride -> [String] -> m (Map String Inspect)
  644. inspects _ [] = return Map.empty
  645. inspects envOverride images =
  646. do maybeInspectOut <-
  647. try (readDockerProcess envOverride ("inspect" : images))
  648. case maybeInspectOut of
  649. Right inspectOut ->
  650. -- filtering with 'isAscii' to workaround @docker inspect@ output containing invalid UTF-8
  651. case eitherDecode (LBS.pack (filter isAscii (decodeUtf8 inspectOut))) of
  652. Left msg -> throwM (InvalidInspectOutputException msg)
  653. Right results -> return (Map.fromList (map (\r -> (iiId r,r)) results))
  654. Left (ReadProcessException _ _ _ err)
  655. | "Error: No such image" `LBS.isPrefixOf` err -> return Map.empty
  656. Left e -> throwM e
  657. -- | Pull latest version of configured Docker image from registry.
  658. pull :: M env m => m ()
  659. pull =
  660. do config <- asks getConfig
  661. let docker = configDocker config
  662. envOverride <- getEnvOverride (configPlatform config)
  663. checkDockerVersion envOverride docker
  664. pullImage envOverride docker (dockerImage docker)
  665. -- | Pull Docker image from registry.
  666. pullImage :: (MonadLogger m,MonadIO m,MonadThrow m,MonadBaseControl IO m)
  667. => EnvOverride -> DockerOpts -> String -> m ()
  668. pullImage envOverride docker image =
  669. do $logInfo (concatT ["Pulling image from registry: '",image,"'"])
  670. when (dockerRegistryLogin docker)
  671. (do $logInfo "You may need to log in."
  672. callProcess $ Cmd
  673. Nothing
  674. "docker"
  675. envOverride
  676. (concat
  677. [["login"]
  678. ,maybe [] (\n -> ["--username=" ++ n]) (dockerRegistryUsername docker)
  679. ,maybe [] (\p -> ["--password=" ++ p]) (dockerRegistryPassword docker)
  680. ,[takeWhile (/= '/') image]]))
  681. e <- try (callProcess (Cmd Nothing "docker" envOverride ["pull",image]))
  682. case e of
  683. Left (ProcessExitedUnsuccessfully _ _) -> throwM (PullFailedException image)
  684. Right () -> return ()
  685. -- | Check docker version (throws exception if incorrect)
  686. checkDockerVersion
  687. :: (MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadCatch m)
  688. => EnvOverride -> DockerOpts -> m ()
  689. checkDockerVersion envOverride docker =
  690. do dockerExists <- doesExecutableExist envOverride "docker"
  691. unless dockerExists (throwM DockerNotInstalledException)
  692. dockerVersionOut <- readDockerProcess envOverride ["--version"]
  693. case words (decodeUtf8 dockerVersionOut) of
  694. (_:_:v:_) ->
  695. case parseVersionFromString (stripVersion v) of
  696. Just v'
  697. | v' < minimumDockerVersion ->
  698. throwM (DockerTooOldException minimumDockerVersion v')
  699. | v' `elem` prohibitedDockerVersions ->
  700. throwM (DockerVersionProhibitedException prohibitedDockerVersions v')
  701. | not (v' `withinRange` dockerRequireDockerVersion docker) ->
  702. throwM (BadDockerVersionException (dockerRequireDockerVersion docker) v')
  703. | otherwise ->
  704. return ()
  705. _ -> throwM InvalidVersionOutputException
  706. _ -> throwM InvalidVersionOutputException
  707. where minimumDockerVersion = $(mkVersion "1.6.0")
  708. prohibitedDockerVersions = []
  709. stripVersion v = fst $ break (== '-') $ dropWhileEnd (not . isDigit) v
  710. -- | Remove the project's Docker sandbox.
  711. reset :: (MonadIO m, MonadReader env m, HasConfig env)
  712. => Maybe (Path Abs Dir) -> Bool -> m ()
  713. reset maybeProjectRoot keepHome = do
  714. dockerSandboxDir <- projectDockerSandboxDir projectRoot
  715. liftIO (removeDirectoryContents
  716. dockerSandboxDir
  717. [homeDirName | keepHome]
  718. [])
  719. where projectRoot = fromMaybeProjectRoot maybeProjectRoot
  720. -- | The Docker container "entrypoint": special actions performed when first entering
  721. -- a container, such as switching the UID/GID to the "outside-Docker" user's.
  722. entrypoint :: (MonadIO m, MonadBaseControl IO m, MonadCatch m, MonadLogger m)
  723. => Config -> DockerEntrypoint -> m ()
  724. entrypoint config@Config{..} DockerEntrypoint{..} =
  725. modifyMVar_ entrypointMVar $ \alreadyRan -> do
  726. -- Only run the entrypoint once
  727. unless alreadyRan $ do
  728. envOverride <- getEnvOverride configPlatform
  729. homeDir <- parseAbsDir =<< liftIO (getEnv "HOME")
  730. -- Get the UserEntry for the 'stack' user in the image, if it exists
  731. estackUserEntry0 <- liftIO $ tryJust (guard . isDoesNotExistError) $
  732. User.getUserEntryForName stackUserName
  733. -- Switch UID/GID if needed, and update user's home directory
  734. case deUser of
  735. Nothing -> return ()
  736. Just (DockerUser 0 _ _ _) -> return ()
  737. Just du -> updateOrCreateStackUser envOverride estackUserEntry0 homeDir du
  738. case estackUserEntry0 of
  739. Left _ -> return ()
  740. Right ue -> do
  741. -- If the 'stack' user exists in the image, copy any build plans and package indices from
  742. -- its original home directory to the host's stack root, to avoid needing to download them
  743. origStackHomeDir <- parseAbsDir (User.homeDirectory ue)
  744. let origStackRoot = origStackHomeDir </> $(mkRelDir ("." ++ stackProgName))
  745. buildPlanDirExists <- doesDirExist (buildPlanDir origStackRoot)
  746. when buildPlanDirExists $ do
  747. (_, buildPlans) <- listDir (buildPlanDir origStackRoot)
  748. forM_ buildPlans $ \srcBuildPlan -> do
  749. let destBuildPlan = buildPlanDir configStackRoot </> filename srcBuildPlan
  750. exists <- doesFileExist destBuildPlan
  751. unless exists $ do
  752. ensureDir (parent destBuildPlan)
  753. copyFile srcBuildPlan destBuildPlan
  754. forM_ configPackageIndices $ \pkgIdx -> do
  755. msrcIndex <- flip runReaderT (config{configStackRoot = origStackRoot}) $ do
  756. srcIndex <- configPackageIndex (indexName pkgIdx)
  757. exists <- doesFileExist srcIndex
  758. return $ if exists
  759. then Just srcIndex
  760. else Nothing
  761. case msrcIndex of
  762. Nothing -> return ()
  763. Just srcIndex -> do
  764. flip runReaderT config $ do
  765. destIndex <- configPackageIndex (indexName pkgIdx)
  766. exists <- doesFileExist destIndex
  767. unless exists $ do
  768. ensureDir (parent destIndex)
  769. copyFile srcIndex destIndex
  770. return True
  771. where
  772. updateOrCreateStackUser envOverride estackUserEntry homeDir DockerUser{..} = do
  773. case estackUserEntry of
  774. Left _ -> do
  775. -- If no 'stack' user in image, create one with correct UID/GID and home directory
  776. readProcessNull Nothing envOverride "groupadd"
  777. ["-o"
  778. ,"--gid",show duGid
  779. ,stackUserName]
  780. readProcessNull Nothing envOverride "useradd"
  781. ["-oN"
  782. ,"--uid",show duUid
  783. ,"--gid",show duGid
  784. ,"--home",toFilePathNoTrailingSep homeDir
  785. ,stackUserName]
  786. Right _ -> do
  787. -- If there is already a 'stack' user in the image, adjust its UID/GID and home directory
  788. readProcessNull Nothing envOverride "usermod"
  789. ["-o"
  790. ,"--uid",show duUid
  791. ,"--home",toFilePathNoTrailingSep homeDir
  792. ,stackUserName]
  793. readProcessNull Nothing envOverride "groupmod"
  794. ["-o"
  795. ,"--gid",show duGid
  796. ,stackUserName]
  797. forM_ duGroups $ \gid -> do
  798. readProcessNull Nothing envOverride "groupadd"
  799. ["-o"
  800. ,"--gid",show gid
  801. ,"group" ++ show gid]
  802. -- 'setuid' to the wanted UID and GID
  803. liftIO $ do
  804. User.setGroupID duGid
  805. #ifndef WINDOWS
  806. PosixUser.setGroups duGroups
  807. #endif
  808. User.setUserID duUid
  809. _ <- Files.setFileCreationMask duUmask
  810. return ()
  811. stackUserName = "stack"::String
  812. -- | MVar used to ensure the Docker entrypoint is performed exactly once
  813. entrypointMVar :: MVar Bool
  814. {-# NOINLINE entrypointMVar #-}
  815. entrypointMVar = unsafePerformIO (newMVar False)
  816. -- | Remove the contents of a directory, without removing the directory itself.
  817. -- This is used instead of 'FS.removeTree' to clear bind-mounted directories, since
  818. -- removing the root of the bind-mount won't work.
  819. removeDirectoryContents :: Path Abs Dir -- ^ Directory to remove contents of
  820. -> [Path Rel Dir] -- ^ Top-level directory names to exclude from removal
  821. -> [Path Rel File] -- ^ Top-level file names to exclude from removal
  822. -> IO ()
  823. removeDirectoryContents path excludeDirs excludeFiles =
  824. do isRootDir <- doesDirExist path
  825. when isRootDir
  826. (do (lsd,lsf) <- listDir path
  827. forM_ lsd
  828. (\d -> unless (dirname d `elem` excludeDirs)
  829. (removeDirRecur d))
  830. forM_ lsf
  831. (\f -> unless (filename f `elem` excludeFiles)
  832. (removeFile f)))
  833. -- | Produce a strict 'S.ByteString' from the stdout of a
  834. -- process. Throws a 'ReadProcessException' exception if the
  835. -- process fails. Logs process's stderr using @$logError@.
  836. readDockerProcess
  837. :: (MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadCatch m)
  838. => EnvOverride -> [String] -> m BS.ByteString
  839. readDockerProcess envOverride = readProcessStdout Nothing envOverride "docker"
  840. -- | Name of home directory within docker sandbox.
  841. homeDirName :: Path Rel Dir
  842. homeDirName = $(mkRelDir "_home/")
  843. -- | Directory where 'stack' executable is bind-mounted in Docker container
  844. hostBinDir :: FilePath
  845. hostBinDir = "/opt/host/bin"
  846. -- | Convenience function to decode ByteString to String.
  847. decodeUtf8 :: BS.ByteString -> String
  848. decodeUtf8 bs = T.unpack (T.decodeUtf8 bs)
  849. -- | Convenience function constructing message for @$log*@.
  850. concatT :: [String] -> Text
  851. concatT = T.pack . concat
  852. -- | Fail with friendly error if project root not set.
  853. fromMaybeProjectRoot :: Maybe (Path Abs Dir) -> Path Abs Dir
  854. fromMaybeProjectRoot = fromMaybe (throw CannotDetermineProjectRootException)
  855. -- | Environment variable that contained the old sandbox ID.
  856. -- | Use of this variable is deprecated, and only used to detect old images.
  857. oldSandboxIdEnvVar :: String
  858. oldSandboxIdEnvVar = "DOCKER_SANDBOX_ID"
  859. -- | Options for 'cleanup'.
  860. data CleanupOpts = CleanupOpts
  861. { dcAction :: !CleanupAction
  862. , dcRemoveKnownImagesLastUsedDaysAgo :: !(Maybe Integer)
  863. , dcRemoveUnknownImagesCreatedDaysAgo :: !(Maybe Integer)
  864. , dcRemoveDanglingImagesCreatedDaysAgo :: !(Maybe Integer)
  865. , dcRemoveStoppedContainersCreatedDaysAgo :: !(Maybe Integer)
  866. , dcRemoveRunningContainersCreatedDaysAgo :: !(Maybe Integer) }
  867. deriving (Show)
  868. -- | Cleanup action.
  869. data CleanupAction = CleanupInteractive
  870. | CleanupImmediate
  871. | CleanupDryRun
  872. deriving (Show)
  873. -- | Parsed result of @docker inspect@.
  874. data Inspect = Inspect
  875. {iiConfig :: ImageConfig
  876. ,iiCreated :: UTCTime
  877. ,iiId :: String
  878. ,iiVirtualSize :: Maybe Integer}
  879. deriving (Show)
  880. -- | Parse @docker inspect@ output.
  881. instance FromJSON Inspect where
  882. parseJSON v =
  883. do o <- parseJSON v
  884. Inspect <$> o .: "Config"
  885. <*> o .: "Created"
  886. <*> o .: "Id"
  887. <*> o .:? "VirtualSize"
  888. -- | Parsed @Config@ section of @docker inspect@ output.
  889. data ImageConfig = ImageConfig
  890. {icEnv :: [String]
  891. ,icEntrypoint :: [String]}
  892. deriving (Show)
  893. -- | Parse @Config@ section of @docker inspect@ output.
  894. instance FromJSON ImageConfig where
  895. parseJSON v =
  896. do o <- parseJSON v
  897. ImageConfig
  898. <$> fmap join (o .:? "Env") .!= []
  899. <*> fmap join (o .:? "Entrypoint") .!= []
  900. -- | Function to get command and arguments to run in Docker container
  901. type GetCmdArgs env m
  902. = M env m
  903. => DockerOpts
  904. -> EnvOverride
  905. -> Inspect
  906. -> Bool
  907. -> m (FilePath,[String],[(String,String)],[Mount])
  908. type M env m = (MonadIO m,MonadReader env m,MonadLogger m,MonadBaseControl IO m,MonadCatch m
  909. ,HasConfig env,HasTerminal env,HasReExec env,HasHttpManager env,MonadMask m)