hadolint-2.14.0: src/Hadolint/Rule/DL3009.hs
module Hadolint.Rule.DL3009 (rule) where
import Hadolint.Rule
import Language.Docker.Syntax
import qualified Data.Map.Strict as Map
import qualified Data.Text as Text
import qualified Hadolint.Shell as Shell
import qualified Hadolint.Utils as Utils
data Acc
= Acc
{ lastFrom :: BaseImage,
dockerClean :: Bool,
stages :: Map.Map Text.Text Linenumber,
forgets :: Map.Map Linenumber BaseImage
}
| Empty
deriving (Eq, Show)
rule :: Rule Shell.ParsedShell
rule = dl3009 <> onbuild dl3009
{-# INLINEABLE rule #-}
dl3009 :: Rule Shell.ParsedShell
dl3009 = veryCustomRule check (emptyState Empty) markFailures
where
code = "DL3009"
severity = DLInfoC
message = "Delete the apt lists (/var/lib/apt/lists) after installing something"
check line st (From from) = st |> modify (rememberStage line from)
check line st (Run (RunArgs args flags))
| not (foldArguments forgotToCleanup args) =
if foldArguments disabledDockerClean args
then st |> modify rememberDockerClean
else st
| Utils.hasCacheOrTmpfsMountWith "/var/lib/apt/lists" flags = st
| Utils.hasCacheOrTmpfsMountWith "/var/lib/apt" flags
&& Utils.hasCacheOrTmpfsMountWith "/var/cache/apt" flags = st
| otherwise = st |> modify (rememberLine line)
check _ st _ = st
-- Convert the final state into failures
markFailures (State fails Empty) = fails
markFailures (State _ Acc {..}) = forgets |> Map.foldMapWithKey mapFail
where
mapFail line from
| from == lastFrom = pure CheckFailure {..}
| BaseImage {alias = Just (ImageAlias als)} <- from, -- Check if this stage is used later
Just _ <- Map.lookup als stages =
-- If the same alias is used in another stage, fail
pure CheckFailure {..}
| otherwise = mempty
{-# INLINEABLE dl3009 #-}
rememberStage :: Linenumber -> BaseImage -> Acc -> Acc
rememberStage line from@BaseImage {image = Image _ als} (Acc _ _ stages o) = Acc from True (Map.insert als line stages) o
rememberStage line from@BaseImage {image = Image _ als} Empty = Acc from True (Map.singleton als line) Map.empty
rememberLine :: Linenumber -> Acc -> Acc
rememberLine line (Acc from clean stages o) = Acc from clean stages (Map.insert line from o)
rememberLine line Empty = Acc emptyImage True mempty (Map.singleton line emptyImage)
rememberDockerClean :: Acc -> Acc
rememberDockerClean Empty = Acc emptyImage False mempty mempty
rememberDockerClean (Acc from _ stages forget) = Acc from False stages forget
forgotToCleanup :: Shell.ParsedShell -> Bool
forgotToCleanup args
| hasUpdate args && not hasCleanup = True
| otherwise = False
where
hasCleanup =
any (Shell.cmdHasArgs "rm" ["-rf", "/var/lib/apt/lists/*"]) (Shell.presentCommands args)
hasUpdate :: Shell.ParsedShell -> Bool
hasUpdate args = any isPackageUpdate (Shell.presentCommands args)
where
isPackageUpdate cmd =
Shell.cmdHasArgs "apt" ["update"] cmd ||
Shell.cmdHasArgs "apt-get" ["update"] cmd ||
Shell.cmdHasArgs "aptitude" ["update"] cmd
disabledDockerClean :: Shell.ParsedShell -> Bool
disabledDockerClean args
| removesScript || keepsPackages = True
| otherwise = False
where
removesScript =
any
(Shell.cmdHasArgs "rm" ["/etc/apt/apt.conf.d/docker-clean"])
(Shell.presentCommands args)
keepsPackages =
any
( Shell.cmdHasArgs
"echo"
["\'Binary::apt::APT::Keep-Downloaded-Packages \"true\";\'"]
)
(Shell.presentCommands args)
-- | Even though dockerfiles without a FROM are not valid, we still want to provide some feedback for this rule
-- so we pretend there is a base image at the start of the file if there is none
emptyImage :: BaseImage
emptyImage = (BaseImage {image = "scratch", tag = Nothing, digest = Nothing, alias = Nothing, platform = Nothing})