packages feed

hadolint-2.2.0: src/Hadolint/Config.hs

module Hadolint.Config
  ( applyConfig,
    ConfigFile (..),
    OverrideConfig (..),
  )
where

import Control.Applicative ((<|>))
import qualified Control.Foldl.Text as Text
import Control.Monad (filterM)
import qualified Data.ByteString as Bytes
import Data.Coerce (coerce)
import Data.Maybe (fromMaybe, listToMaybe)
import qualified Data.Set as Set
import Data.YAML ((.:?))
import qualified Data.YAML as Yaml
import GHC.Generics (Generic)
import qualified Hadolint.Lint as Lint
import qualified Hadolint.Process as Process
import qualified Hadolint.Rule as Rule
import qualified Language.Docker as Docker
import System.Directory
  ( XdgDirectory (..),
    doesFileExist,
    getCurrentDirectory,
    getXdgDirectory,
  )
import System.FilePath ((</>))

data OverrideConfig = OverrideConfig
  { overrideErrorRules :: Maybe [Lint.ErrorRule],
    overrideWarningRules :: Maybe [Lint.WarningRule],
    overrideInfoRules :: Maybe [Lint.InfoRule],
    overrideStyleRules :: Maybe [Lint.StyleRule]
  }
  deriving (Show, Eq, Generic)

instance Semigroup OverrideConfig where
  OverrideConfig a1 a2 a3 a4 <> OverrideConfig b1 b2 b3 b4 =
    OverrideConfig (a1 <> b1) (a2 <> b2) (a3 <> b3) (a4 <> b4)

instance Monoid OverrideConfig where
  mempty = OverrideConfig Nothing Nothing Nothing Nothing

data ConfigFile = ConfigFile
  { overrideRules :: Maybe OverrideConfig,
    ignoredRules :: Maybe [Lint.IgnoreRule],
    trustedRegistries :: Maybe [Lint.TrustedRegistry],
    labelSchemaConfig :: Maybe Rule.LabelSchema,
    strictLabelSchema :: Maybe Bool
  }
  deriving (Show, Eq, Generic)

instance Yaml.FromYAML OverrideConfig where
  parseYAML = Yaml.withMap "OverrideConfig" $ \m ->
    OverrideConfig
      <$> m .:? "error"
      <*> m .:? "warning"
      <*> m .:? "info"
      <*> m .:? "style"

instance Yaml.FromYAML ConfigFile where
  parseYAML = Yaml.withMap "ConfigFile" $ \m -> do
    overrideRules <- m .:? "override"
    ignored <- m .:? "ignored"
    let ignoredRules = coerce (ignored :: Maybe [Text.Text])
    trustedRegistries <- m .:? "trustedRegistries"
    labelSchemaConfig <- m .:? "label-schema"
    strictLabelSchema <- m .:? "strict-labels"
    return ConfigFile {..}

-- | If both the ignoreRules and rulesConfig properties of Lint options are empty
-- then this function will fill them with the default found in the passed config
-- file. If there is an error parsing the default config file, this function will
-- return the error string.
applyConfig :: Maybe FilePath -> Lint.LintOptions -> IO (Either String Lint.LintOptions)
applyConfig maybeConfig o
  | not (Prelude.null (Lint.ignoreRules o)) && Lint.rulesConfig o /= mempty = return (Right o)
  | otherwise = do
    theConfig <-
      case maybeConfig of
        Nothing -> findConfig
        c -> return c
    case theConfig of
      Nothing -> return (Right o)
      Just config -> parseAndApply config
  where
    findConfig = do
      localConfigFile <- (</> ".hadolint.yaml") <$> getCurrentDirectory
      configFile <- getXdgDirectory XdgConfig "hadolint.yaml"
      listToMaybe <$> filterM doesFileExist [localConfigFile, configFile]

    parseAndApply :: FilePath -> IO (Either String Lint.LintOptions)
    parseAndApply configFile = do
      contents <- Bytes.readFile configFile
      return $ case Yaml.decode1Strict contents of
        Left (_, err) -> Left (formatError err configFile)
        Right config -> Right $ fromMaybe o (applyOverride config)

    applyOverride ConfigFile {..} =
      -- Maybe.do
      do
        OverrideConfig {..} <- overrideRules <|> Just mempty
        overrideError <- overrideErrorRules <|> Just mempty
        overrideWarning <- overrideWarningRules <|> Just mempty
        overrideInfo <- overrideInfoRules <|> Just mempty
        overrideStyle <- overrideStyleRules <|> Just mempty
        overrideIgnored <- ignoredRules <|> Just mempty

        trusted <- Set.fromList . coerce <$> (trustedRegistries <|> Just mempty)
        schema <- labelSchemaConfig <|> Just mempty
        strictLabels <- strictLabelSchema <|> Just False

        let rulesConfig = Lint.rulesConfig o

        return $
          Lint.LintOptions
            { Lint.errorRules = Lint.errorRules o <|> overrideError,
              Lint.warningRules = Lint.warningRules o <|> overrideWarning,
              Lint.infoRules = Lint.infoRules o <|> overrideInfo,
              Lint.styleRules = Lint.styleRules o <|> overrideStyle,
              Lint.ignoreRules = Lint.ignoreRules o <|> overrideIgnored,
              Lint.rulesConfig =
                Process.RulesConfig
                  { Process.allowedRegistries = Process.allowedRegistries rulesConfig `ifNull` trusted,
                    Process.labelSchema = Process.labelSchema rulesConfig `ifNull` schema,
                    Process.strictLabels = Process.strictLabels rulesConfig || strictLabels
                  }
            }

    ifNull value override = if null value then override else value

    formatError err config =
      Prelude.unlines
        [ "Error parsing your config file in  '" ++ config ++ "':",
          "It should contain one of the keys 'override', 'ignored'",
          "or 'trustedRegistries'. For example:\n",
          "ignored:",
          "\t- DL3000",
          "\t- SC1099\n\n",
          "The key 'override' should contain only lists with names 'error',",
          "'warning', 'info' or 'style', which each name rules to override the",
          "severity on. For example:\n",
          "override:",
          "\terror:",
          "\t\t- DL3008\n\n",
          "The key 'trustedRegistries' should contain the names of the allowed docker registries:\n",
          "trustedRegistries:",
          "\t- docker.io",
          "\t- my-company.com",
          "",
          err
        ]