packages feed

language-puppet-1.5.1: src/PuppetDB/Core.hs

{-# LANGUAGE TemplateHaskell #-}

module PuppetDB.Core where

import Control.Lens
import Data.Aeson
import qualified Data.List as List
import Data.Maybe (fromJust)
import qualified Data.Maybe.Strict as S
import Data.Time.Clock
import Facter
import GHC.Read (Read (..))
import Puppet.Language
import Web.HttpApiData (ToHttpApiData (..))
import XPrelude hiding (Read)

-- | The supported PuppetDB implementations.
data PDBType
  = -- | Your standard PuppetDB, queried through the HTTP interface.
    PDBRemote
  | -- | A stupid stub, this is the default choice.
    PDBDummy
  | -- | A slow but handy PuppetDB implementation that is backed by a YAML file.
    PDBTest
  deriving (Eq)

instance Read PDBType where
  readsPrec _ r
    | isJust reml = [(PDBRemote, fromJust reml)]
    | isJust rems = [(PDBRemote, fromJust rems)]
    | isJust duml = [(PDBDummy, fromJust duml)]
    | isJust dums = [(PDBDummy, fromJust dums)]
    | isJust tstl = [(PDBTest, fromJust tstl)]
    | isJust tsts = [(PDBTest, fromJust tsts)]
    | otherwise = []
    where
      reml = List.stripPrefix "PDBRemote" r
      rems = List.stripPrefix "remote" r
      duml = List.stripPrefix "PDBDummy" r
      dums = List.stripPrefix "dummy" r
      tstl = List.stripPrefix "PDBTest" r
      tsts = List.stripPrefix "test" r

data NodeInfo = NodeInfo
  { _nodeInfoName :: !NodeName,
    _nodeInfoDeactivated :: !Bool,
    _nodeInfoCatalogT :: !(S.Maybe UTCTime),
    _nodeInfoFactsT :: !(S.Maybe UTCTime),
    _nodeInfoReportT :: !(S.Maybe UTCTime)
  }

makeClassy ''NodeInfo

instance ToJSON NodeInfo where
  toJSON p =
    object
      [ ("name", toJSON (p ^. nodeInfoName)),
        ("deactivated", toJSON (p ^. nodeInfoDeactivated)),
        ("catalog_timestamp", toJSON (p ^. nodeInfoCatalogT)),
        ("facts_timestamp", toJSON (p ^. nodeInfoFactsT)),
        ("report_timestamp", toJSON (p ^. nodeInfoReportT))
      ]

instance FromJSON NodeInfo where
  parseJSON (Object v) =
    NodeInfo
      <$> v .: "name"
      <*> v .:? "deactivated" .!= False
      <*> v .: "catalog_timestamp"
      <*> v .: "facts_timestamp"
      <*> v .: "report_timestamp"
  parseJSON _ = fail "invalide node info"

-- | Pretty straightforward way to define the various PuppetDB queries
data Query a
  = QEqual a Text
  | QG a Integer
  | QL a Integer
  | QGE a Integer
  | QLE a Integer
  | QMatch Text Text
  | QAnd [Query a]
  | QOr [Query a]
  | QNot (Query a)
  | QEmpty

instance (ToJSON a) => ToJSON (Query a) where
  toJSON (QOr qs) = toJSON ("or" : map toJSON qs)
  toJSON (QAnd qs) = toJSON ("and" : map toJSON qs)
  toJSON (QNot q) = toJSON ["not", toJSON q]
  toJSON (QEqual flds val) = toJSON ["=", toJSON flds, toJSON val]
  toJSON (QMatch flds val) = toJSON ["~", toJSON flds, toJSON val]
  toJSON (QL flds val) = toJSON ["<", toJSON flds, toJSON val]
  toJSON (QG flds val) = toJSON [">", toJSON flds, toJSON val]
  toJSON (QLE flds val) = toJSON ["<=", toJSON flds, toJSON val]
  toJSON (QGE flds val) = toJSON [">=", toJSON flds, toJSON val]
  toJSON QEmpty = Null

instance (ToJSON a) => ToHttpApiData (Query a) where
  toHeader = Control.Lens.view strict . encode
  toUrlPiece = decodeUtf8 . toHeader

instance (FromJSON a) => FromJSON (Query a) where
  parseJSON Null = pure QEmpty
  parseJSON (Array elems) = case toList elems of
    ("or" : xs) -> QOr <$> mapM parseJSON xs
    ("and" : xs) -> QAnd <$> mapM parseJSON xs
    ["not", x] -> QNot <$> parseJSON x
    ["=", flds, val] -> QEqual <$> parseJSON flds <*> parseJSON val
    ["~", flds, val] -> QEqual <$> parseJSON flds <*> parseJSON val
    [">", flds, val] -> QG <$> parseJSON flds <*> parseJSON val
    ["<", flds, val] -> QL <$> parseJSON flds <*> parseJSON val
    [">=", flds, val] -> QGE <$> parseJSON flds <*> parseJSON val
    ["<=", flds, val] -> QLE <$> parseJSON flds <*> parseJSON val
    x -> fail ("unknown query" ++ show x)
  parseJSON _ = fail "Expected an array"

-- | Fields for the fact endpoint
data FactField
  = FName
  | FValue
  | FCertname

instance ToJSON FactField where
  toJSON FName = "name"
  toJSON FValue = "value"
  toJSON FCertname = "certname"

instance FromJSON FactField where
  parseJSON "name" = pure FName
  parseJSON "value" = pure FValue
  parseJSON "certname" = pure FCertname
  parseJSON _ = fail "Can't parse fact field"

-- | Fields for the node endpoint
data NodeField = NName | NFact Text

instance ToJSON NodeField where
  toJSON NName = "name"
  toJSON (NFact t) = toJSON ["fact", t]

instance FromJSON NodeField where
  parseJSON (Array xs) = case toList xs of
    ["fact", x] -> NFact <$> parseJSON x
    _ -> fail "Invalid field syntax"
  parseJSON (String "name") = pure NName
  parseJSON _ = fail "invalid field"

-- | Fields for the resource endpoint
data ResourceField
  = RTag
  | RCertname
  | RParameter Text
  | RType
  | RTitle
  | RExported
  | RFile
  | RLine

instance ToJSON ResourceField where
  toJSON RTag = "tag"
  toJSON RCertname = "certname"
  toJSON (RParameter t) = toJSON ["parameter", t]
  toJSON RType = "type"
  toJSON RTitle = "title"
  toJSON RExported = "exported"
  toJSON RFile = "file"
  toJSON RLine = "line"

instance FromJSON ResourceField where
  parseJSON (Array xs) =
    case toList xs of
      ["parameter", x] -> RParameter <$> parseJSON x
      _ -> fail "Invalid field syntax"
  parseJSON (String "tag") = pure RTag
  parseJSON (String "certname") = pure RCertname
  parseJSON (String "type") = pure RType
  parseJSON (String "title") = pure RTitle
  parseJSON (String "exported") = pure RExported
  parseJSON (String "file") = pure RFile
  parseJSON (String "line") = pure RLine
  parseJSON _ = fail "invalid field"

data PuppetDBAPI m = PuppetDBAPI
  { pdbInformation :: m Doc,
    -- | <http://docs.puppetlabs.com/puppetdb/1.5/api/commands.html#replace-catalog-version-3>
    replaceCatalog :: WireCatalog -> ExceptT PrettyError m (),
    -- | <http://docs.puppetlabs.com/puppetdb/1.5/api/commands.html#replace-facts-version-1>
    replaceFacts :: [(NodeName, Facts)] -> ExceptT PrettyError m (),
    -- | <http://docs.puppetlabs.com/puppetdb/1.5/api/commands.html#deactivate-node-version-1>
    deactivateNode :: NodeName -> ExceptT PrettyError m (),
    -- | <http://docs.puppetlabs.com/puppetdb/1.5/api/query/v3/facts.html#get-v3facts>
    getPDBFacts :: Query FactField -> ExceptT PrettyError m [FactInfo],
    -- | <http://docs.puppetlabs.com/puppetdb/1.5/api/query/v3/resources.html#get-v3resources>
    getResources :: Query ResourceField -> ExceptT PrettyError m [Resource],
    getNodes :: Query NodeField -> ExceptT PrettyError m [NodeInfo],
    -- | This is only here to tell the test PuppetDB to save its content to disk.
    commitDB :: ExceptT PrettyError m (),
    getResourcesOfNode :: NodeName -> Query ResourceField -> ExceptT PrettyError m [Resource]
  }