packages feed

language-puppet 0.1.7.2 → 0.1.8.0

raw patch · 9 files changed

+233/−52 lines, 9 filesdep +failure

Dependencies added: failure

Files

Erb/Compute.hs view
@@ -18,7 +18,7 @@ type TemplateAnswer = Either String String  initTemplateDaemon :: Prefs -> IO (String -> String -> [(String, GeneralValue)] -> IO (Either String String))-initTemplateDaemon (Prefs _ modpath templatepath _ _) = do+initTemplateDaemon (Prefs _ modpath templatepath _ _ _) = do     controlchan <- newChan     forkIO (templateDaemon modpath templatepath controlchan)     return (templateQuery controlchan)@@ -77,4 +77,5 @@ toRuby' (ResolvedBool False) = "false" toRuby' (ResolvedArray rr) = "[" ++ intercalate ", " (map toRuby' rr) ++ "]" toRuby' (ResolvedHash hh) = "{ " ++ intercalate ", " (map (\(varname, varval) -> show varname ++ " => " ++ toRuby' varval) hh) ++ " }"+toRuby' ResolvedUndefined = ":undef" toRuby' x = show x
Puppet/Daemon.hs view
@@ -19,6 +19,7 @@ import qualified Data.List.Utils as DLU import qualified Data.Map as Map import Text.Parsec.Pos (initialPos)+import PuppetDB.Rest  -- this daemon returns a catalog when asked for a node and facts data DaemonMessage@@ -50,6 +51,12 @@ It is recommended to ask for as many parser and interpreter threads as there are CPUs. +It can optionnaly talk with PuppetDB, by setting an URL in the 'Prefs' data+structure. The recommended way to set it to http://localhost:8080 and set a SSH+tunnel :++> ssh -L 8080:localhost:8080 puppet.host+ Known bugs :  * It might be buggy when top level statements that are not class/define/nodes@@ -86,7 +93,10 @@     case message of         QCatalog (nodename, facts, respchan) -> do             logDebug ("Received query for node " ++ nodename)-            (stmts, warnings) <- getCatalog getstmts gettemplate nodename facts+            let pdbfunc = case (puppetDBurl prefs) of+                              Just x  -> Just (pdbResRequest x)+                              Nothing -> Nothing+            (stmts, warnings) <- getCatalog getstmts gettemplate pdbfunc nodename facts             mapM_ logWarning warnings             case stmts of                 Left x -> writeChan respchan (RCatalog $ Left x)
Puppet/Init.hs view
@@ -9,14 +9,15 @@     modules :: FilePath, -- ^ The path to the modules.     templates :: FilePath, -- ^ The path to the template.     compilepoolsize :: Int, -- ^ Size of the compiler pool.-    parsepoolsize :: Int -- ^ Size of the parser pool.+    parsepoolsize :: Int, -- ^ Size of the parser pool.+    puppetDBurl :: Maybe String -- ^ Url of the PuppetDB connector (must be cleartext). } deriving (Show)  -- | Generates the 'Prefs' structure from a single path. -- -- > genPrefs "/etc/puppet" genPrefs :: String -> Prefs-genPrefs basedir = Prefs (basedir ++ "/manifests") (basedir ++ "/modules") (basedir ++ "/templates") 1 1+genPrefs basedir = Prefs (basedir ++ "/manifests") (basedir ++ "/modules") (basedir ++ "/templates") 1 1 Nothing  -- | Generates 'Facts' from pairs of strings. --
Puppet/Interpreter/Catalog.hs view
@@ -38,11 +38,12 @@ import Puppet.Interpreter.Functions import Puppet.Interpreter.Types import Puppet.Printers+import qualified PuppetDB.Query as PDB  import System.IO.Unsafe import Data.List import Data.Char (isDigit,toLower,toUpper, isAlpha, isAlphaNum, isSpace)-import Data.Maybe (isJust, fromJust, catMaybes)+import Data.Maybe (isJust, fromJust, catMaybes, isNothing) import Data.Either (lefts, rights, partitionEithers) import Data.Ord (comparing) import Text.Parsec.Pos@@ -78,14 +79,33 @@     -> (String -> String -> [(String, GeneralValue)] -> IO (Either String String))     -- ^ The \"get template\" function. Given a file name, a scope name and a     -- list of variables, it should return the computed template.+    -> Maybe (String -> PDB.Query -> IO (Either String [CResource]))+    -- ^ The \"puppetDB Rest API\" function. Given the machine fqdn, a request+    -- type (resources, nodes, facts, ..) and a query, it returns a+    -- ResolvedValue, or some error.     -> String -- ^ Name of the node.     -> Facts -- ^ Facts of this node.     -> IO (Either String FinalCatalog, [String])-getCatalog getstatements gettemplate nodename facts = do+getCatalog getstatements gettemplate puppetdb nodename facts = do     let convertedfacts = Map.map             (\fval -> (Right fval, initialPos "FACTS"))             facts-    (output, finalstate) <- runStateT ( runErrorT ( computeCatalog getstatements nodename ) ) (ScopeState [["::"]] convertedfacts Map.empty [] 1 (initialPos "dummy") Map.empty getstatements [] [] [] gettemplate)+    (output, finalstate) <- runStateT ( runErrorT ( computeCatalog getstatements nodename ) )+                                (ScopeState+                                   { curScope                   = [["::"]]+                                   , curVariables               = convertedfacts+                                   , curClasses                 = Map.empty+                                   , curDefaults                = []+                                   , curResId                   = 1+                                   , curPos                     = (initialPos "dummy")+                                   , nestedtoplevels            = Map.empty+                                   , getStatementsFunction      = getstatements+                                   , getWarnings                = []+                                   , curCollect                 = []+                                   , unresolvedRels             = []+                                   , computeTemplateFunction    = gettemplate+                                   , puppetDBFunction           = puppetdb+                                   } )     case output of         Left x -> return (Left x, getWarnings finalstate)         Right _ -> return (output, getWarnings finalstate)@@ -130,7 +150,7 @@         else do             -- Note that amending attributes with a collector does collect virtual             -- values. Hence no filtering on the collectors is done here.-            isCollected <- liftM curCollect get >>= mapM (\(x, _) -> x res)+            isCollected <- liftM curCollect get >>= mapM (\(x, _, _) -> x res)             case (or isCollected, crvirtuality res) of                 (True, Exported)    -> return [res { crvirtuality = Normal }, res]                 (True,  _)          -> return [res { crvirtuality = Normal }     ]@@ -138,9 +158,9 @@  processOverride :: CResource -> Map.Map String ResolvedValue -> CatalogMonad (Map.Map String ResolvedValue) processOverride cr prms =-    let applyOverride :: CResource -> Map.Map String ResolvedValue -> (CResource -> CatalogMonad Bool, [(GeneralString, GeneralValue)]) -> CatalogMonad (Map.Map String ResolvedValue)+    let applyOverride :: CResource -> Map.Map String ResolvedValue -> (CResource -> CatalogMonad Bool, [(GeneralString, GeneralValue)], Maybe PDB.Query) -> CatalogMonad (Map.Map String ResolvedValue)         -- this checks if the collection function matches-        applyOverride c prm (func, overs) = do+        applyOverride c prm (func, overs, _) = do             check <- func c             if check                 then foldM tryReplace prm overs@@ -153,13 +173,37 @@             rv <- resolveGeneralValue gv             return $ Map.insert rs rv curmap     -- Collectors are filtered so that only those with overrides are passed to the fold.-    in liftM (filter (not . null . snd) . curCollect) get >>= foldM (applyOverride cr) prms+    in liftM (filter (\(_, x, _) -> not $ null x) . curCollect) get >>= foldM (applyOverride cr) prms +retrieveRemoteResources :: (PDB.Query -> IO (Either String [CResource])) -> PDB.Query -> CatalogMonad [CResource]+retrieveRemoteResources f q = do+    res <- liftIO $ f q+    hashes <- case res of+        Right h     -> return h+        Left err    -> throwError $ "PuppetDB error: " ++ err+    return hashes +extractRelations :: CResource -> CatalogMonad CResource+extractRelations cr = do+    let (params, rels) = partitionParamsRelations (crparams cr)+    -- TODO export relations+    return cr { crparams = params }+ finalResolution :: Catalog -> CatalogMonad FinalCatalog finalResolution cat = do-    --liftIO $ putStrLn $ "FINAL RESOLUTION"-    collected <- mapM collectionChecks cat >>= mapM evaluateDefine . concat+    pdbfunction <- fmap puppetDBFunction get+    fqdnr       <- getVariable "::fqdn"+    collectedRemote <- case pdbfunction of+                           Just f -> do+                               fqdn <- case fqdnr of+                                   Just (Right (ResolvedString f), _) -> return f+                                   _ -> throwError "Could not get FQDN during final resolution"+                               remoteCollects <- fmap (catMaybes . map (\(_,_,x) -> x) . curCollect) get+                               fmap concat (mapM (retrieveRemoteResources (f fqdn)) remoteCollects)+                           Nothing -> return []+    collectedRemote' <- mapM extractRelations collectedRemote+    collectedLocal <- fmap concat (mapM collectionChecks cat)+    collected <- mapM evaluateDefine (collectedLocal ++ collectedRemote')     let (real,  allvirtual)  = partition (\x -> crvirtuality x == Normal)  (concat collected)         (_,  exported) = partition (\x -> crvirtuality x == Virtual)  allvirtual     --export stuff@@ -231,7 +275,7 @@         nstate = curstate { nestedtoplevels = ntop }     put nstate addWarning = modify . pushWarning-addCollect = modify . pushCollect+addCollect ((func, query), overrides) = modify $ pushCollect (func, overrides, query) -- this pushes the relations only if they exist -- the parameter is of the form -- ( [dstrelations], srcresource, type, pos )@@ -326,16 +370,23 @@ evaluateDefine r@(CResource _ rname rtype rparams rvirtuality rpos) = let     evaluateDefineDeclaration dtype args dstmts dpos = do         --oldpos <- getPos-        setPos dpos         pushScope ["#DEFINE#" ++ dtype]         -- add variables         mrrparams <- mapM (\(gs, gv) -> do { rgs <- resolveGeneralString gs; rgv <- tryResolveGeneralValue gv; return (rgs, (rgv, dpos)); }) rparams         let expr = gs2gv rname             mparams = Map.fromList mrrparams+            defineparamset = Set.fromList $ map fst args+            mandatoryparams = Set.fromList $ map fst $ filter (isNothing . snd) args+            resourceparamset = Set.fromList $ map fst mrrparams+            extraparams = Set.difference resourceparamset (Set.union defineparamset metaparameters)+            unsetparams = Set.difference mandatoryparams resourceparamset+        unless (Set.null extraparams) $ throwPosError $ "Spurious parameters set for " ++ dtype ++ ": " ++ intercalate ", " (Set.toList extraparams)+        unless (Set.null unsetparams) $ throwPosError $ "Unset parameters set for " ++ dtype ++ ": " ++ intercalate ", " (Set.toList unsetparams)         putVariable "title" (expr, rpos)         putVariable "name" (expr, rpos)         mapM_ (loadClassVariable rpos mparams) args +        setPos dpos         -- parse statements         res <- mapM evaluateStatements dstmts         nres <- handleDelayedActions (concat res)@@ -903,7 +954,7 @@         myfunction (CResource _ mcrname mcrtype _ _ _) = do             srname <- resolveGeneralString mcrname             return ((srname == rname) && (mcrtype == rtype))-    addCollect (myfunction, [])+    addCollect ((myfunction, Just $ PDB.queryRealize rtype rname) , [])     return () pushRealize (ResolvedRReference _ x) = throwPosError (show x ++ " was not resolved to a string") pushRealize x                        = throwPosError ("A reference was expected instead of " ++ show x)@@ -1022,10 +1073,10 @@ gs2gv (Left e)  = Left e gs2gv (Right s) = Right $ ResolvedString s -collectionFunction :: Virtuality -> String -> Expression -> CatalogMonad (CResource -> CatalogMonad Bool)+collectionFunction :: Virtuality -> String -> Expression -> CatalogMonad (CResource -> CatalogMonad Bool, Maybe PDB.Query) collectionFunction virt mrtype exprs = do-    finalfunc <- case exprs of-        BTrue -> return (\_ -> return True)+    (finalfunc, pdbquery) <- case exprs of+        BTrue -> return (\_ -> return True, Just (PDB.collectAll mrtype))         EqualOperation a b -> do             ra <- resolveExpression a             rb <- resolveExpression b@@ -1052,6 +1103,10 @@                                 let filtered = filter (compareRValues rb) xs                                 in  return $ not $ null filtered                             _ -> return $ compareRValues cmp rb+                , case (paramname, rb) of+                      ("tag", ResolvedString tagval) -> Just (PDB.collectTag mrtype tagval)+                      (param, ResolvedString prmval) -> Just (PDB.collectParam mrtype param prmval)+                      _                              -> Nothing                 )         x -> throwPosError $ "TODO : implement collection function for " ++ show x     return (\res -> do@@ -1059,7 +1114,10 @@         if (crtype res == mrtype) && ( ((virt == Virtual) &&  (crvirtuality res == Normal)) || (crvirtuality res == virt))             then finalfunc res             else return False-            )+        , if (virt == Exported)+              then pdbquery+              else Nothing+        )   generalValue2Expression :: GeneralValue -> Expression
Puppet/Interpreter/Types.hs view
@@ -1,6 +1,7 @@ module Puppet.Interpreter.Types where  import Puppet.DSL.Types+import qualified PuppetDB.Query as PDB import Text.Parsec.Pos import Control.Monad.State import Control.Monad.Error@@ -118,17 +119,20 @@     -- ^ This is a function that, given the type of a top level statement and     -- its name, should return it.     getWarnings :: ![String], -- ^ List of warnings.-    curCollect :: ![(CResource -> CatalogMonad Bool, [(GeneralString, GeneralValue)])],+    curCollect :: ![(CResource -> CatalogMonad Bool, [(GeneralString, GeneralValue)], Maybe PDB.Query)],     -- ^ A bit complicated, this stores the collection functions. These are     -- functions that determine whether a resource should be collected or not.     -- It can optionally store overrides, which will be applied in the end on-    -- all resources.+    -- all resources. It can also store a PuppetDB query.     unresolvedRels :: ![([(LinkType, GeneralValue, GeneralValue)], (String, GeneralString), RelUpdateType, SourcePos)],     -- ^ This stores unresolved relationships, because the original string name     -- can't be resolved. Fieds are [ ( [dstrelations], srcresource, type, pos ) ]-    computeTemplateFunction :: String -> String -> [(String, GeneralValue)] -> IO (Either String String)+    computeTemplateFunction :: String -> String -> [(String, GeneralValue)] -> IO (Either String String),     -- ^ Function that takes a filename, the current scope and a list of     -- variables. It returns an error or the computed template.+    puppetDBFunction :: Maybe (String -> PDB.Query -> IO (Either String [CResource]))+    -- ^ Function that takes a fqdn, request type (resources, nodes, facts, ..)+    -- and a query, and returns a resolved value from puppetDB. }  -- | The monad all the interpreter lives in. It is 'ErrorT' with a state.@@ -144,7 +148,7 @@ generalizeStringS = Right  -- |This is the set of meta parameters-metaparameters = Set.fromList ["tag","stage","name","title","alias","audit","check","loglevel","noop","schedule"]+metaparameters = Set.fromList ["tag","stage","name","title","alias","audit","check","loglevel","noop","schedule", "EXPORTEDSOURCE"]  getPos               = liftM curPos get modifyScope     f sc = sc { curScope       = f $ curScope sc }
+ PuppetDB/Query.hs view
@@ -0,0 +1,67 @@+module PuppetDB.Query where++import Data.List (intercalate)+import Data.Char (toUpper)+import Data.String.Utils (split)++data Operator = OEqual | OOver | OUnder | OOverE | OUnderE | OAnd | OOr | ONot+    deriving (Show, Ord, Eq)++-- [Field] Value+data Query = Query Operator [Query] | Term String | Terms [String]+    deriving (Show, Ord, Eq)++capitalize :: String -> String+capitalize = intercalate "::" . map capitalize' . split "::"++capitalize' :: String -> String+capitalize' "" = ""+capitalize' (x:xs) = toUpper x : xs++-- | Query used for realizing a resources, when its type and title is known.+queryRealize :: String -> String -> Query+queryRealize rtype rtitle = Query OAnd+                                [ Query OEqual [ Term "type",       Term (capitalize rtype) ]+                                , Query OEqual [ Term "title",      Term rtitle ]+                                , Query OEqual [ Term "exported",   Term "true" ]+                                ]++-- | Collects all resources of a given type+collectAll :: String -> Query+collectAll rtype = Query OAnd [ Query OEqual [ Term "type",     Term (capitalize rtype) ]+                              , Query OEqual [ Term "exported", Term "true" ]+                              ]++-- | Collections based on tags.+collectTag :: String -> String -> Query+collectTag rtype tagval = Query OAnd+                                [ Query OEqual [ Term "type",     Term (capitalize rtype) ]+                                , Query OEqual [ Term "tag" ,     Term tagval ]+                                , Query OEqual [ Term "exported", Term "true" ]+                                ]++-- | Used to emulate collections such as `Type<|| prmname == "prmval" ||>`+collectParam :: String -> String -> String -> Query+collectParam rtype prmname prmval = Query OAnd+                                    [ Query OEqual [ Term "type",                    Term (capitalize rtype) ]+                                    , Query OEqual [ Terms ["parameter", "prmname"], Term prmval ]+                                    , Query OEqual [ Term "exported",                Term "true" ]+                                    ]++dq :: String -> String+dq x = '"' : x ++ "\""++showOperator :: Operator -> String+showOperator OEqual     = dq "="+showOperator OOver      = dq ">"+showOperator OUnder     = dq "<"+showOperator OOverE     = dq ">="+showOperator OUnderE    = dq "<="+showOperator OAnd       = dq "and"+showOperator OOr        = dq "or"+showOperator ONot       = dq "not"++showQuery :: Query -> String+showQuery (Query op subqueries) = "[" ++ intercalate ", " (showOperator op : map showQuery subqueries) ++ "]"+showQuery (Term t)              = show t+showQuery (Terms ts)            = show ts
PuppetDB/Rest.hs view
@@ -2,8 +2,9 @@  module PuppetDB.Rest where +import qualified Puppet.DSL.Types as DT import Puppet.Interpreter.Types-import Puppet.Printers+import qualified PuppetDB.Query as PDB  import Network.HTTP.Conduit import qualified Network.HTTP.Types as W@@ -15,6 +16,10 @@ import qualified Data.Text as T import Data.Attoparsec.Number import qualified Codec.Text.IConv as IConv+import qualified Control.Exception as X+import Control.Monad.Error+import Control.Applicative+import qualified Text.Parsec.Pos as TPP  instance FromJSON ResolvedValue where     parseJSON Null = return ResolvedUndefined@@ -29,29 +34,64 @@                                                                  ) (HM.toList o))     parseJSON (Bool b) = return $ ResolvedBool b -toAndQuery = undefined+instance FromJSON CResource where+    parseJSON (Object o) = do+        utitle     <- o .: "title"+        params     <- o .: "parameters"+        sourcefile <- o .: "sourcefile"+        sourceline <- o .: "sourceline"+        certname   <- o .: "certname"+        let _ = params :: HM.HashMap String ResolvedValue+            parameters = map (\(k,v) -> (Right k, Right v)) $ ("EXPORTEDSOURCE", ResolvedString certname) : HM.toList params :: [(GeneralString, GeneralValue)]+            position   = TPP.newPos (sourcefile ++ "(host: " ++ certname ++ ")") sourceline 1+        CResource <$> pure 0+                  <*> pure (Right utitle)+                  <*> fmap (T.unpack . T.toLower) (o .: "type")+                  <*> pure parameters+                  <*> pure DT.Normal+                  <*> pure position+    parseJSON _ = mzero -simpleNodeQuery :: String -> [(String, String)] -> IO (Either String ResolvedValue)-simpleNodeQuery url query = rawRequest url "nodes" (toAndQuery query)+runRequest req = do+    let doRequest = withManager (\manager -> fmap responseBody $ httpLbs req manager) :: IO L.ByteString+        eHandler :: X.SomeException -> IO (Either String  L.ByteString)+        eHandler e = return $ Left $ show e ++ ", with queryString " ++ (BC.unpack $ queryString req)+    mo <- liftIO ((fmap Right doRequest) `X.catch` eHandler)+    case mo of+        Right o -> do+            let utf8 = IConv.convert "LATIN1" "UTF-8" o+            case decode' utf8 of+                Just x                   -> return x+                Nothing                  -> throwError "Json decoding has failed"+        Left err -> throwError err -simpleResourceQuery :: String -> [(String, String)] -> IO (Either String ResolvedValue)-simpleResourceQuery url query = rawRequest url "resources" (toAndQuery query)+isNotLocal :: String -> CResource -> Bool+isNotLocal fqdn cr =+    let prms = crparams cr+        e    = filter ((== Right "EXPORTEDSOURCE") . fst ) prms :: [(GeneralString, GeneralValue)]+    in case e of+        [(_, Right (ResolvedString x))] -> x /= fqdn+        _ -> True -rawRequest :: String -> String -> String -> IO (Either String ResolvedValue)-rawRequest url querytype query = do-        let q = BC.unpack $ W.renderSimpleQuery False [("query", BC.pack query)]-            pfunc = parseUrl (url ++ "/" ++ querytype ++ "?" ++ q)-        eInitReq <- case querytype of-                       "resources"  -> fmap Right pfunc-                       "nodes"      -> fmap Right pfunc-                       _            -> return $ Left $ "Invalid query type " ++ querytype-        case eInitReq of-            Right initReq -> do-                let req = initReq { requestHeaders = [("Accept", "application/json")] }-                o <- withManager (\manager -> fmap responseBody $ httpLbs req manager) :: IO L.ByteString-                let utf8 = IConv.convert "LATIN1" "UTF-8" o-                case decode' utf8 :: Maybe ResolvedValue of-                    Just x@(ResolvedArray _) -> return $ Right x-                    Just x                   -> return $ Left $ "PuppetDB should have returned an array, not " ++ showValue x-                    Nothing                  -> return $ Left "Json decoding has failed"-            Left err -> return $ Left err+pdbResRequest :: String -> String -> PDB.Query -> IO (Either String [CResource])+pdbResRequest url fqdn qquery = do+        res <- rawRequest url "resources" (PDB.showQuery qquery)+        case res of+            Left x -> return $ Left x+            Right y -> return $ Right $ filter ( isNotLocal fqdn ) y++pdbRequest :: String -> String -> PDB.Query -> IO (Either String ResolvedValue)+pdbRequest url querytype qquery = rawRequest url querytype (PDB.showQuery qquery)++rawRequest :: (FromJSON a) => String -> String -> String -> IO (Either String a)+rawRequest url querytype query = runErrorT $ do+        unless (querytype `elem` ["resources", "nodes", "facts"]) (throwError $ "Invalid query type " ++ querytype)+        let q = case querytype of+                    "facts" -> '/' : query+                    _       -> "?" ++ (BC.unpack $ W.renderSimpleQuery False [("query", BC.pack query)])+            fullurl = url ++ "/" ++ querytype ++ q+        initReq <- case (parseUrl fullurl :: Maybe (Request a)) of+            Just x -> return x+            Nothing -> throwError "Something failed when parsing the PuppetDB URL"+        let req = initReq { requestHeaders = [("Accept", "application/json")] }+        runRequest req
language-puppet.cabal view
@@ -7,7 +7,7 @@ -- The package version. See the Haskell package versioning policy -- (http://www.haskell.org/haskellwiki/Package_versioning_policy) for -- standards guiding when and how versions should be incremented.-Version:             0.1.7.2+Version:             0.1.8.0  -- A short (one-line) description of the package. Synopsis:            Tools to parse and evaluate the Puppet DSL.@@ -52,11 +52,11 @@     ghc-options:       -Wall -fno-warn-missing-signatures -fno-warn-unused-do-bind -funbox-strict-fields   -- Modules exported by the library.   Exposed-modules:     Puppet.DSL.Parser, Puppet.DSL.Printer, Puppet.Daemon, Puppet.Init, Puppet.DSL.Loader, Puppet.Printers, Puppet.NativeTypes, Puppet.DSL.Types,-                       Puppet.Interpreter.Types, Puppet.Interpreter.Catalog, Puppet.NativeTypes.Helpers, PuppetDB.Rest+                       Puppet.Interpreter.Types, Puppet.Interpreter.Catalog, Puppet.NativeTypes.Helpers, PuppetDB.Rest, PuppetDB.Query    -- Packages needed in order to build this package.   Build-depends:       base >=3 && <5,parsec,MissingH,containers,pretty,mtl,unix,hslogger,filepath,Glob,regexpr,process,bytestring,cryptohash,base16-bytestring,regex-pcre-builtin,-                        iconv, text, unordered-containers, aeson, vector, http-types, http-conduit, attoparsec+                        iconv, text, unordered-containers, aeson, vector, http-types, http-conduit, attoparsec, failure    -- Modules not exported by this package.   Other-modules:       Puppet.Interpreter.Functions, Puppet.NativeTypes.File, Erb.Compute, SafeProcess, Paths_language_puppet, Erb.Parser, Erb.Ruby, Erb.Evaluate
ruby/calcerb.rb view
@@ -15,7 +15,7 @@         elsif has_variable?(@context + "::" + name)             @mvars[@context + "::" + name]         else-            :undef+            throw("Unknown variable " + name)         end     end