{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE TemplateHaskell #-} module Puppet.Language.Resource ( Resource(..) , HasResource(..) , RIdentifier(..) , HasRIdentifier(..) , LinkInformation(..) , HasLinkInformation(..) , LinkType(..) , Virtuality(..) , CurContainerDesc(..) , FinalCatalog , EdgeMap ) where import XPrelude import Data.Aeson import qualified Data.HashMap.Strict as Map import qualified Data.HashSet as Set import qualified Data.Text as Text import qualified Data.Vector as Vector import qualified GHC.Exts as Exts import qualified Text.Megaparsec.Pos as Pos import Puppet.Language.Core import Puppet.Language.Value rel2text :: LinkType -> Text rel2text RNotify = "notify" rel2text RRequire = "require" rel2text RBefore = "before" rel2text RSubscribe = "subscribe" data Virtuality = Normal -- ^ Normal resource, that will be included in the catalog. | Virtual -- ^ Type for virtual resources. | Exported -- ^ Type for exported resources. | ExportedRealized -- ^ These are resources that are exported AND realized in the catalog. deriving (Eq, Show) data CurContainerDesc = ContRoot -- ^ Contained at node or root level. | ContClass !Text -- ^ Contained in a class. | ContDefine !Text !Text !PPosition -- ^ Contained in a define, along with the position where this define was ... defined | ContImported !CurContainerDesc -- ^ Dummy container for imported resources, so that we know we must update the nodename | ContImport !NodeName !CurContainerDesc -- ^ This one is used when finalizing imported resources, and contains the current node name deriving (Eq, Generic, Ord, Show) instance Pretty CurContainerDesc where pretty (ContImport p x) = magenta "import" <> braces (ppline p) <> braces (pretty x) pretty (ContImported x) = magenta "imported" <> braces (pretty x) pretty ContRoot = dullyellow "::" pretty (ContClass cname) = dullyellow "class" <+> dullgreen (ppline cname) pretty (ContDefine dtype dname _) = pretty (PResourceReference dtype dname) -- | Relationship/ordering between resources. data LinkType = RRequire -- ^ Applies a resource after the target resource. | RBefore -- ^ Applies a resource before the target resource. | RNotify -- ^ Applies a resource before the target resource. The target resource refreshes if the notifying resource changes. | RSubscribe -- ^ Applies a resource after the target resource. The subscribing resource refreshes if the target resource changes. deriving (Show, Eq, Generic) instance Hashable LinkType instance FromJSON LinkType where parseJSON (String "require") = return RRequire parseJSON (String "notify") = return RNotify parseJSON (String "subscribe") = return RSubscribe parseJSON (String "before") = return RBefore parseJSON _ = panic "invalid linktype" instance ToJSON LinkType where toJSON = String . rel2text instance Pretty LinkType where pretty RNotify = "~>" pretty RRequire = "<-" pretty RBefore = "->" pretty RSubscribe = "<~" -- | In Puppet, a resource is identified by a name and a type. data RIdentifier = RIdentifier { _itype :: !Text , _iname :: !Text } deriving (Show, Eq, Generic, Ord) instance Pretty RIdentifier where pretty (RIdentifier t n) = pretty (PResourceReference t n) instance FromJSON RIdentifier where parseJSON (Object v) = RIdentifier <$> v .: "type" <*> v .: "title" parseJSON _ = fail "invalid resource" instance ToJSON RIdentifier where toJSON (RIdentifier t n) = object [("type", String t), ("title", String n)] instance Hashable RIdentifier -- | A fully resolved puppet resource that will be used in the 'FinalCatalog'. data Resource = Resource { _rid :: !RIdentifier -- ^ Resource name. , _ralias :: !(HashSet Text) -- ^ All the resource aliases , _rattributes :: !(Container PValue) -- ^ Resource parameters. , _rrelations :: !(HashMap RIdentifier (HashSet LinkType)) -- ^ Resource relations. , _rscope :: ![CurContainerDesc] -- ^ Resource scope when it was defined, the real container will be the first item , _rvirtuality :: !Virtuality -- ^ Virtuality. , _rtags :: !(HashSet Text) -- ^ Tags. , _rpos :: !PPosition -- ^ Source code position of the resource definition. , _rnode :: !NodeName -- ^ The node were this resource was created, if remote } deriving (Eq, Show) makeClassy ''Resource makeClassy ''RIdentifier resourceRelations :: Resource -> [(RIdentifier, LinkType)] resourceRelations = concatMap expandSet . Map.toList . view rrelations where expandSet (ri, lts) = [(ri, lt) | lt <- Set.toList lts] meta :: Resource -> Doc meta r = showPPos (r ^. rpos) <+> red node <+> green (brackets scp) where node = ppline (r ^. rnode) scp = "Scope" <+> pretty (r ^.. rscope . folded . filtered (/=ContRoot) . to pretty) resourceBody :: Resource -> Doc resourceBody r = virtuality <> blue (ppline (r ^. rid . iname)) <> ":" <+> meta r <> line <> containerComma'' insde <> ";" where virtuality = case r ^. rvirtuality of Normal -> mempty Virtual -> dullred "@" Exported -> dullred "@@" ExportedRealized -> dullred "<@@>" insde = alignlst dullblue attriblist1 ++ alignlst dullmagenta attriblist2 alignlst col = map (first (fill maxalign . col . ppline)) attriblist1 = Exts.sortWith fst $ Map.toList (r ^. rattributes) ++ aliasdiff aliasWithoutTitle = r ^. ralias & contains (r ^. rid . iname) .~ False aliasPValue = aliasWithoutTitle & PArray . Vector.fromList . map PString . Set.toList aliasdiff | Set.null aliasWithoutTitle = [] | otherwise = [("alias", aliasPValue)] attriblist2 = map totext (resourceRelations r) totext (RIdentifier t n, lt) = (rel2text lt , PResourceReference t n) maxalign = max (maxalign' attriblist1) (maxalign' attriblist2) maxalign' [] = 0 maxalign' x = maximum . map (Text.length . fst) $ x instance Pretty Resource where prettyList lst = let grouped = Map.toList $ Map.fromListWith (++) [ (r ^. rid . itype, [r]) | r <- lst ] :: [ (Text, [Resource]) ] sorted = Exts.sortWith fst (map (second (Exts.sortWith (view (rid.iname)))) grouped) showGroup :: (Text, [Resource]) -> Doc showGroup (rt, res) = dullyellow (ppline rt) <+> lbrace <> line <> indent 2 (vcat (map resourceBody res)) <> line <> rbrace in vcat (map showGroup sorted) pretty r = dullyellow (ppline (r ^. rid . itype)) <+> lbrace <> line <> indent 2 (resourceBody r) <> line <> rbrace instance ToJSON Resource where toJSON r = object [ ("type", String $ r ^. rid . itype) , ("title", String $ r ^. rid . iname) , ("aliases", toJSON $ r ^. ralias) , ("exported", Bool $ r ^. rvirtuality == Exported) , ("tags", toJSON $ r ^. rtags) , ("parameters", Object ( fmap toJSON (r ^. rattributes) `Map.union` relations )) , ("sourceline", r ^. rpos . _1 . _sourceLine . to (toJSON . Pos.unPos)) , ("sourcefile", r ^. rpos . _1 . _sourceName . to toJSON) ] where relations = r ^. rrelations & Map.fromListWith (Vector.++) . concatMap changeRelations . Map.toList & fmap toValue toValue v | Vector.length v == 1 = Vector.head v | otherwise = Array v changeRelations :: (RIdentifier, HashSet LinkType) -> [(Text, Vector Value)] changeRelations (k,v) = do c <- Set.toList v return (rel2text c, Vector.singleton (String (rid2text k))) rid2text :: RIdentifier -> Text rid2text (RIdentifier t n) = capitalizeRT t `Text.append` "[" `Text.append` capn `Text.append` "]" where capn = if t == "classe" then capitalizeRT n else n instance FromJSON Resource where parseJSON (Object v) = do isExported <- v .: "exported" let virtuality = if isExported then Exported else Normal getResourceIdentifier :: PValue -> Maybe RIdentifier getResourceIdentifier (PString x) = let (restype, brckts) = Text.breakOn "[" x rna | Text.null brckts = Nothing | Text.null restype = Nothing | Text.last brckts == ']' = Just (Text.tail (Text.init brckts)) | otherwise = Nothing in case rna of Just resname -> Just (RIdentifier (Text.toLower restype) (Text.toLower resname)) _ -> Nothing getResourceIdentifier _ = Nothing -- TODO : properly handle metaparameters separate :: (Container PValue, HashMap RIdentifier (HashSet LinkType)) -> Text -> PValue -> (Container PValue, HashMap RIdentifier (HashSet LinkType)) separate (curAttribs, curRelations) k val = case (fromJSON (String k), getResourceIdentifier val) of (Success rel, Just ri) -> (curAttribs, curRelations & at ri . non mempty . contains rel .~ True) _ -> (curAttribs & at k ?~ val, curRelations) (attribs,relations) <- Map.foldlWithKey' separate (mempty,mempty) <$> v .: "parameters" contimport <- v .:? "certname" .!= "unknown" Resource <$> (RIdentifier <$> fmap Text.toLower (v .: "type") <*> v .: "title") <*> v .:? "aliases" .!= mempty <*> pure attribs <*> pure relations <*> pure [ContImport contimport ContRoot] <*> pure virtuality <*> v .: "tags" <*> (toPPos <$> v .:? "sourcefile" .!= "null" <*> v .:? "sourceline" .!= 1) <*> pure contimport parseJSON _ = mempty type FinalCatalog = HashMap RIdentifier Resource -- | Relationship/ordering information between two resources (used in the 'EdgeMap'). data LinkInformation = LinkInformation { _linksrc :: !RIdentifier , _linkdst :: !RIdentifier , _linkType :: !LinkType , _linkPos :: !PPosition } deriving (Show) makeClassy ''LinkInformation type EdgeMap = HashMap RIdentifier [LinkInformation]