{-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} -- | Docker types. module Stack.Types.Docker ( DockerException (..) , DockerMonoidRepoOrImage (..) , DockerOpts (..) , DockerOptsMonoid (..) , DockerStackExe (..) , Mount (..) , VersionRangeJSON (..) , dockerAutoPullArgName , dockerCmdName , dockerContainerNameArgName , dockerContainerPlatform , dockerDetachArgName , dockerEnableArgName , dockerEntrypointArgName , dockerEnvArgName , dockerHelpOptName , dockerImageArgName , dockerMountArgName , dockerMountModeArgName , dockerNetworkArgName , dockerPersistArgName , dockerPullCmdName , dockerRegistryLoginArgName , dockerRegistryPasswordArgName , dockerRegistryUsernameArgName , dockerRepoArgName , dockerRequireDockerVersionArgName , dockerRunArgsArgName , dockerSetUserArgName , dockerStackExeArgName , dockerStackExeDownloadVal , dockerStackExeHostVal , dockerStackExeImageVal , parseDockerStackExe , reExecArgName ) where import Data.List ( intercalate ) import qualified Data.Text as T import Distribution.System ( Platform (..), OS (..), Arch (..) ) import Distribution.Text ( simpleParse, display ) import Distribution.Version ( anyVersion ) import Generics.Deriving.Monoid ( mappenddefault, memptydefault ) import Pantry.Internal.AesonExtended import Path import Stack.Constants ( stackProgName ) import Stack.Prelude hiding ( Display (..) ) import Stack.Types.Version import Text.Read ( Read (..) ) -- | Type representing exceptions thrown by functions exported by the -- "Stack.Docker" module. data DockerException = DockerMustBeEnabledException -- ^ Docker must be enabled to use the command. | OnlyOnHostException -- ^ Command must be run on host OS (not in a container). | InspectFailedException String -- ^ @docker inspect@ failed. | NotPulledException String -- ^ Image does not exist. | InvalidImagesOutputException String -- ^ Invalid output from @docker images@. | InvalidPSOutputException String -- ^ Invalid output from @docker ps@. | InvalidInspectOutputException String -- ^ Invalid output from @docker inspect@. | PullFailedException String -- ^ Could not pull a Docker image. | DockerTooOldException Version Version -- ^ Installed version of @docker@ below minimum version. | DockerVersionProhibitedException [Version] Version -- ^ Installed version of @docker@ is prohibited. | BadDockerVersionException VersionRange Version -- ^ Installed version of @docker@ is out of range specified in config file. | InvalidVersionOutputException -- ^ Invalid output from @docker --version@. | HostStackTooOldException Version (Maybe Version) -- ^ Version of @stack@ on host is too old for version in image. | ContainerStackTooOldException Version Version -- ^ Version of @stack@ in container/image is too old for version on host. | CannotDetermineProjectRootException -- ^ Can't determine the project root (where to put docker sandbox). | DockerNotInstalledException -- ^ @docker --version@ failed. | UnsupportedStackExeHostPlatformException -- ^ Using host stack-exe on unsupported platform. | DockerStackExeParseException String -- ^ @stack-exe@ option fails to parse. deriving (Show, Typeable) instance Exception DockerException where displayException DockerMustBeEnabledException = "Error: [S-3223]\n" ++ "Docker must be enabled in your configuration file to use this \ \command." displayException OnlyOnHostException = "Error: [S-9779]\n" ++ "This command must be run on host OS (not in a Docker container)." displayException (InspectFailedException image) = concat [ "Error: [S-9105]\n" , "'docker inspect' failed for image after pull: " , image , "." ] displayException (NotPulledException image) = concat [ "Error: [S-6626]\n" , "The Docker image referenced by your configuration file" , " has not\nbeen downloaded:\n " , image , "\n\nRun '" , unwords [stackProgName, dockerCmdName, dockerPullCmdName] , "' to download it, then try again." ] displayException (InvalidImagesOutputException l) = concat [ "Error: [S-5841]\n" , "Invalid 'docker images' output line: '" , l , "'." ] displayException (InvalidPSOutputException l) = concat [ "Error: [S-9608]\n" , "Invalid 'docker ps' output line: '" , l ,"'." ] displayException (InvalidInspectOutputException msg) = concat [ "Error: [S-2240]\n" , "Invalid 'docker inspect' output: " , msg , "." ] displayException (PullFailedException image) = concat [ "Error: [S-6092]\n" , "Could not pull Docker image:\n " , image , "\nThere may not be an image on the registry for your resolver's LTS \ \version in\n" , "your configuration file." ] displayException (DockerTooOldException minVersion haveVersion) = concat [ "Error: [S-6281]\n" , "Minimum docker version '" , versionString minVersion , "' is required by " , stackProgName , " (you have '" , versionString haveVersion , "')." ] displayException (DockerVersionProhibitedException prohibitedVersions haveVersion) = concat [ "Error: [S-8252]\n" , "These Docker versions are incompatible with " , stackProgName , " (you have '" , versionString haveVersion , "'): " , intercalate ", " (map versionString prohibitedVersions) , "." ] displayException (BadDockerVersionException requiredRange haveVersion) = concat [ "Error: [S-6170]\n" , "The version of 'docker' you are using (" , show haveVersion , ") is outside the required\n" , "version range specified in stack.yaml (" , T.unpack (versionRangeText requiredRange) , ")." ] displayException InvalidVersionOutputException = "Error: [S-5827]\n" ++ "Cannot get Docker version (invalid 'docker --version' output)." displayException (HostStackTooOldException minVersion (Just hostVersion)) = concat [ "Error: [S-7112]\n" , "The host's version of '" , stackProgName , "' is too old for this Docker image.\nVersion " , versionString minVersion , " is required; you have " , versionString hostVersion , "." ] displayException (HostStackTooOldException minVersion Nothing) = concat [ "Error: [S-7112]\n" , "The host's version of '" , stackProgName , "' is too old.\nVersion " , versionString minVersion , " is required." ] displayException (ContainerStackTooOldException requiredVersion containerVersion) = concat [ "Error: [S-5832]\n" , "The Docker container's version of '" , stackProgName , "' is too old.\nVersion " , versionString requiredVersion , " is required; the container has " , versionString containerVersion , "." ] displayException CannotDetermineProjectRootException = "Error: [S-4078]\n" ++ "Cannot determine project root directory for Docker sandbox." displayException DockerNotInstalledException = "Error: [S-7058]\n" ++ "Cannot find 'docker' in PATH. Is Docker installed?" displayException UnsupportedStackExeHostPlatformException = concat [ "Error: [S-6894]\n" , "Using host's " , stackProgName , " executable in Docker container is only supported on " , display dockerContainerPlatform , " platform." ] displayException (DockerStackExeParseException s) = concat [ "Error: [S-1512]\n" , "Failed to parse " , show s , ". Expected " , show dockerStackExeDownloadVal , ", " , show dockerStackExeHostVal , ", " , show dockerStackExeImageVal , " or absolute path to executable." ] -- | Docker configuration. data DockerOpts = DockerOpts { dockerEnable :: !Bool -- ^ Is using Docker enabled? , dockerImage :: !(Either SomeException String) -- ^ Exact Docker image tag or ID. Overrides docker-repo-*/tag. , dockerRegistryLogin :: !Bool -- ^ Does registry require login for pulls? , dockerRegistryUsername :: !(Maybe String) -- ^ Optional username for Docker registry. , dockerRegistryPassword :: !(Maybe String) -- ^ Optional password for Docker registry. , dockerAutoPull :: !Bool -- ^ Automatically pull new images. , dockerDetach :: !Bool -- ^ Whether to run a detached container , dockerPersist :: !Bool -- ^ Create a persistent container (don't remove it when finished). Implied by -- `dockerDetach`. , dockerContainerName :: !(Maybe String) -- ^ Container name to use, only makes sense from command-line with `dockerPersist` -- or `dockerDetach`. , dockerNetwork :: !(Maybe String) -- ^ The network docker uses. , dockerRunArgs :: ![String] -- ^ Arguments to pass directly to @docker run@. , dockerMount :: ![Mount] -- ^ Volumes to mount in the container. , dockerMountMode :: !(Maybe String) -- ^ Volume mount mode , dockerEnv :: ![String] -- ^ Environment variables to set in the container. , dockerStackExe :: !(Maybe DockerStackExe) -- ^ Location of container-compatible Stack executable , dockerSetUser :: !(Maybe Bool) -- ^ Set in-container user to match host's , dockerRequireDockerVersion :: !VersionRange -- ^ Require a version of Docker within this range. } deriving Show -- | An uninterpreted representation of docker options. -- Configurations may be "cascaded" using mappend (left-biased). data DockerOptsMonoid = DockerOptsMonoid { dockerMonoidDefaultEnable :: !Any -- ^ Should Docker be defaulted to enabled (does @docker:@ section exist in the config)? , dockerMonoidEnable :: !(First Bool) -- ^ Is using Docker enabled? , dockerMonoidRepoOrImage :: !(First DockerMonoidRepoOrImage) -- ^ Docker repository name (e.g. @fpco/stack-build@ or @fpco/stack-full:lts-2.8@) , dockerMonoidRegistryLogin :: !(First Bool) -- ^ Does registry require login for pulls? , dockerMonoidRegistryUsername :: !(First String) -- ^ Optional username for Docker registry. , dockerMonoidRegistryPassword :: !(First String) -- ^ Optional password for Docker registry. , dockerMonoidAutoPull :: !FirstTrue -- ^ Automatically pull new images. , dockerMonoidDetach :: !FirstFalse -- ^ Whether to run a detached container , dockerMonoidPersist :: !FirstFalse -- ^ Create a persistent container (don't remove it when finished). Implied by -- `dockerDetach`. , dockerMonoidContainerName :: !(First String) -- ^ Container name to use, only makes sense from command-line with `dockerPersist` -- or `dockerDetach`. , dockerMonoidNetwork :: !(First String) -- ^ See: 'dockerNetwork' , dockerMonoidRunArgs :: ![String] -- ^ Arguments to pass directly to @docker run@ , dockerMonoidMount :: ![Mount] -- ^ Volumes to mount in the container , dockerMonoidMountMode :: !(First String) -- ^ Volume mount mode , dockerMonoidEnv :: ![String] -- ^ Environment variables to set in the container , dockerMonoidStackExe :: !(First DockerStackExe) -- ^ Location of container-compatible Stack executable , dockerMonoidSetUser :: !(First Bool) -- ^ Set in-container user to match host's , dockerMonoidRequireDockerVersion :: !IntersectingVersionRange -- ^ See: 'dockerRequireDockerVersion' } deriving (Show, Generic) -- | Decode uninterpreted docker options from JSON/YAML. instance FromJSON (WithJSONWarnings DockerOptsMonoid) where parseJSON = withObjectWarnings "DockerOptsMonoid" (\o -> do let dockerMonoidDefaultEnable = Any True dockerMonoidEnable <- First <$> o ..:? dockerEnableArgName dockerMonoidRepoOrImage <- First <$> ( (Just . DockerMonoidImage <$> o ..: dockerImageArgName) <|> (Just . DockerMonoidRepo <$> o ..: dockerRepoArgName) <|> pure Nothing ) dockerMonoidRegistryLogin <- First <$> o ..:? dockerRegistryLoginArgName dockerMonoidRegistryUsername <- First <$> o ..:? dockerRegistryUsernameArgName dockerMonoidRegistryPassword <- First <$> o ..:? dockerRegistryPasswordArgName dockerMonoidAutoPull <- FirstTrue <$> o ..:? dockerAutoPullArgName dockerMonoidDetach <- FirstFalse <$> o ..:? dockerDetachArgName dockerMonoidPersist <- FirstFalse <$> o ..:? dockerPersistArgName dockerMonoidContainerName <- First <$> o ..:? dockerContainerNameArgName dockerMonoidNetwork <- First <$> o ..:? dockerNetworkArgName dockerMonoidRunArgs <- o ..:? dockerRunArgsArgName ..!= [] dockerMonoidMount <- o ..:? dockerMountArgName ..!= [] dockerMonoidMountMode <- First <$> o ..:? dockerMountModeArgName dockerMonoidEnv <- o ..:? dockerEnvArgName ..!= [] dockerMonoidStackExe <- First <$> o ..:? dockerStackExeArgName dockerMonoidSetUser <- First <$> o ..:? dockerSetUserArgName dockerMonoidRequireDockerVersion <- IntersectingVersionRange . unVersionRangeJSON <$> ( o ..:? dockerRequireDockerVersionArgName ..!= VersionRangeJSON anyVersion ) pure DockerOptsMonoid{..}) -- | Left-biased combine Docker options instance Semigroup DockerOptsMonoid where (<>) = mappenddefault -- | Left-biased combine Docker options instance Monoid DockerOptsMonoid where mempty = memptydefault mappend = (<>) -- | Where to get the `stack` executable to run in Docker containers data DockerStackExe = DockerStackExeDownload -- ^ Download from official bindist | DockerStackExeHost -- ^ Host's `stack` (linux-x86_64 only) | DockerStackExeImage -- ^ Docker image's `stack` (versions must match) | DockerStackExePath (Path Abs File) -- ^ Executable at given path deriving Show instance FromJSON DockerStackExe where parseJSON a = do s <- parseJSON a case parseDockerStackExe s of Right dse -> pure dse Left e -> fail (displayException e) -- | Parse 'DockerStackExe'. parseDockerStackExe :: (MonadThrow m) => String -> m DockerStackExe parseDockerStackExe t | t == dockerStackExeDownloadVal = pure DockerStackExeDownload | t == dockerStackExeHostVal = pure DockerStackExeHost | t == dockerStackExeImageVal = pure DockerStackExeImage | otherwise = case parseAbsFile t of Just p -> pure (DockerStackExePath p) Nothing -> throwM (DockerStackExeParseException t) -- | Docker volume mount. data Mount = Mount String String -- | For optparse-applicative. instance Read Mount where readsPrec _ s = case break (== ':') s of (a,':':b) -> [(Mount a b,"")] (a,[]) -> [(Mount a a,"")] _ -> fail "Invalid value for Docker mount (expect '/host/path:/container/path')" -- | Show instance. instance Show Mount where show (Mount a b) = if a == b then a else concat [a,":",b] -- | For YAML. instance FromJSON Mount where parseJSON v = do s <- parseJSON v case readMaybe s of Nothing -> fail $ "Mount read failed: " ++ s Just x -> pure x -- | Options for Docker repository or image. data DockerMonoidRepoOrImage = DockerMonoidRepo String | DockerMonoidImage String deriving Show -- | Newtype for non-orphan FromJSON instance. newtype VersionRangeJSON = VersionRangeJSON { unVersionRangeJSON :: VersionRange } -- | Parse VersionRange. instance FromJSON VersionRangeJSON where parseJSON = withText "VersionRange" (\s -> maybe (fail ("Invalid cabal-style VersionRange: " ++ T.unpack s)) (pure . VersionRangeJSON) (Distribution.Text.simpleParse (T.unpack s))) -- | Docker enable argument name. dockerEnableArgName :: Text dockerEnableArgName = "enable" -- | Docker repo arg argument name. dockerRepoArgName :: Text dockerRepoArgName = "repo" -- | Docker image argument name. dockerImageArgName :: Text dockerImageArgName = "image" -- | Docker registry login argument name. dockerRegistryLoginArgName :: Text dockerRegistryLoginArgName = "registry-login" -- | Docker registry username argument name. dockerRegistryUsernameArgName :: Text dockerRegistryUsernameArgName = "registry-username" -- | Docker registry password argument name. dockerRegistryPasswordArgName :: Text dockerRegistryPasswordArgName = "registry-password" -- | Docker auto-pull argument name. dockerAutoPullArgName :: Text dockerAutoPullArgName = "auto-pull" -- | Docker detach argument name. dockerDetachArgName :: Text dockerDetachArgName = "detach" -- | Docker run args argument name. dockerRunArgsArgName :: Text dockerRunArgsArgName = "run-args" -- | Docker mount argument name. dockerMountArgName :: Text dockerMountArgName = "mount" -- | Docker mount mode argument name. dockerMountModeArgName :: Text dockerMountModeArgName = "mount-mode" -- | Docker environment variable argument name. dockerEnvArgName :: Text dockerEnvArgName = "env" -- | Docker container name argument name. dockerContainerNameArgName :: Text dockerContainerNameArgName = "container-name" -- -- | Docker container name argument name. dockerNetworkArgName :: Text dockerNetworkArgName = "network" -- | Docker persist argument name. dockerPersistArgName :: Text dockerPersistArgName = "persist" -- | Docker Stack executable argument name. dockerStackExeArgName :: Text dockerStackExeArgName = "stack-exe" -- | Value for @--docker-stack-exe=download@ dockerStackExeDownloadVal :: String dockerStackExeDownloadVal = "download" -- | Value for @--docker-stack-exe=host@ dockerStackExeHostVal :: String dockerStackExeHostVal = "host" -- | Value for @--docker-stack-exe=image@ dockerStackExeImageVal :: String dockerStackExeImageVal = "image" -- | Docker @set-user@ argument name dockerSetUserArgName :: Text dockerSetUserArgName = "set-user" -- | Docker @require-version@ argument name dockerRequireDockerVersionArgName :: Text dockerRequireDockerVersionArgName = "require-docker-version" -- | Argument name used to pass docker entrypoint data (only used internally) dockerEntrypointArgName :: String dockerEntrypointArgName = "internal-docker-entrypoint" -- | Command-line argument for "docker" dockerCmdName :: String dockerCmdName = "docker" dockerHelpOptName :: String dockerHelpOptName = dockerCmdName ++ "-help" -- | Command-line argument for @docker pull@. dockerPullCmdName :: String dockerPullCmdName = "pull" -- | Command-line option for @--internal-re-exec-version@. reExecArgName :: String reExecArgName = "internal-re-exec-version" -- | Platform that Docker containers run dockerContainerPlatform :: Platform dockerContainerPlatform = Platform X86_64 Linux