packages feed

craft-0.0.0.1: src/Craft/Craftable.hs

{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE MultiParamTypeClasses  #-}
{-# LANGUAGE UndecidableInstances   #-}
module Craft.Craftable where

import           Control.Lens
import           Control.Monad.Reader     (ask)
import qualified Data.ByteString.Lazy     as BL
import           Data.List                (intercalate)
import           Data.Maybe               (catMaybes, isJust)
import           Formatting               hiding (char)

import           Craft.DSL
import qualified Craft.File               as File
import           Craft.Helpers
import           Craft.Internal.Helpers
import           Craft.Internal.UserGroup
import           Craft.Types
import qualified Craft.User               as User


data Watched
  = Unchanged
  | Created
  | Updated
  | Removed
  deriving (Eq, Show)

makePrisms ''Watched

changed :: Watched -> Bool
changed = not . unchanged


unchanged :: Watched -> Bool
unchanged Unchanged = True
unchanged _         = False


created :: Watched -> Bool
created Created = True
created _       = False


updated :: Watched -> Bool
updated Updated = True
updated _       = False


removed :: Watched -> Bool
removed Removed = True
removed _       = False


watch :: Eq a => Craft (Maybe a) -> Craft b -> Craft (Watched, Maybe a, b)
watch getter action = do
  !beforeMB <- getter
  !result   <- action
  !afterMB  <- getter
  let (w, x) = case (beforeMB, afterMB) of
                 (Nothing,     Nothing)    -> (Unchanged, Nothing)
                 (Just y,      Nothing)    -> (Removed,   Just y)
                 (Nothing,     Just y)     -> (Created,   Just y)
                 (Just before, Just after) -> if before == after
                                                then (Unchanged, Just after)
                                                else (Updated,   Just after)
  return (w, x, result)


watch_ :: Eq a => Craft (Maybe a) -> Craft b -> Craft Watched
watch_ getter action = do
  (w, _, _) <- watch getter action
  return w


class Craftable a b | a -> b where
  watchCraft :: a -> Craft (Watched, b)

  craft :: a -> Craft b
  craft x = snd <$> watchCraft x

  craft_ :: a -> Craft ()
  craft_ = void . craft

  watchCraft_ :: a -> Craft Watched
  watchCraft_ x = fst <$> watchCraft x

  {-# MINIMAL watchCraft #-}


class Destroyable a where
  watchDestroy :: a -> Craft (Watched, Maybe a)

  destroy :: a -> Craft (Maybe a)
  destroy x = snd <$> watchDestroy x

  destroy_ :: a -> Craft ()
  destroy_ = void . destroy

  watchDestroy_ :: a -> Craft Watched
  watchDestroy_ x = fst <$> watchDestroy x

  {-# MINIMAL watchDestroy #-}


instance Craftable a b => Craftable [a] [b] where
  craft_ = mapM_ craft_
  craft = mapM craft
  watchCraft as = do
    (ws, bs) <- unzip <$> mapM watchCraft as
    let w = if all (== Unchanged) ws then
              Unchanged
            else if all (== Created) ws then
              Created
            else
              Updated
    return (w, bs)


instance Destroyable a => Destroyable [a] where
  destroy_ = mapM_ destroy_
  destroy xs = do
    rs <- mapM destroy xs
    return $ case catMaybes rs of
               []  -> Nothing
               rs' -> Just rs'
  watchDestroy xs = do
    (ws, rs) <- unzip <$> mapM watchDestroy xs
    let w = if all (== Unchanged) ws then
              Unchanged
            else if all (== Removed) ws then
              Removed
            else
              Updated
    let res = case catMaybes rs of
                []  -> Nothing
                rs' -> Just rs'
    return (w, res)


instance Craftable User.UserOptions User where
  watchCraft uopts = do
    let notfound = "craft `"++uopts ^. User.optName++"` failed. Not Found!"
    let name = UserName $ uopts ^. User.optName
    User.fromName name >>= \case
      Nothing           -> do
        User.createUser uopts
        User.fromName name >>= \case
          Nothing          -> $craftError notfound
          Just createdUser -> do
            madeChanges <- User.ensureUserOpts createdUser uopts
            if not madeChanges
              then return (Created, createdUser)
              else User.fromName name >>= \case
                Nothing -> $craftError notfound
                Just u  -> return (Created, u)
      Just existingUser -> do
        madeChanges <- User.ensureUserOpts existingUser uopts
        if not madeChanges
          then return (Unchanged, existingUser)
          else User.fromName name >>= \case
            Nothing -> $craftError notfound
            Just u  -> return (Updated, u)

instance Craftable Package Package where
  watchCraft pkg = do
    ce <- ask
    let pm       = ce ^. craftPackageManager
    let name     = pkg ^. pkgName
    let version  = pkg ^. pkgVersion
    let get      = (pm ^. pmGetter) name
    let install  = (pm ^. pmInstaller) pkg
    let upgrade  = (pm ^. pmUpgrader) pkg
    let pkgError = "craft Package `" ++ name ++ "` failed! "
    let notFound = pkgError ++ "Not Found."
    let wrongVersion got = pkgError ++ "Wrong Version: " ++ show got ++ " Excepted: " ++ show version
    get >>= \case                                                                -- Is the package installed?
      Nothing           -> do                                                    -- It's not installed.
        install                                                                  -- Install it.
        get >>= \case                                                            -- Verify the installation.
          Nothing           -> $craftError notFound                              -- Not Found. The install failed!
          Just installedPkg ->
            let ok = return (Created, installedPkg)
            in case version of                                                   -- Make sure it's the right version
                 AnyVersion -> ok
                 Latest     -> ok
                 Version  _ -> if version == installedPkg ^. pkgVersion
                                then ok
                                else $craftError $ wrongVersion (installedPkg ^. pkgVersion)
      Just installedPkg -> do                                                    -- Package was already installed.
        let installedVersion = installedPkg ^. pkgVersion
        case version of
          AnyVersion -> return (Unchanged, installedPkg)
          Latest     -> do                                                       -- Ensure it's the latest version.
            upgrade
            get >>= \case
              Nothing          -> $craftError notFound                           -- Not found. Where did it go?
              Just upgradedPkg ->
                return $ if upgradedPkg ^. pkgVersion > installedPkg ^. pkgVersion -- If the package version increased,
                          then (Updated, upgradedPkg)                            -- Then the package was upgraded
                          else (Unchanged, upgradedPkg)                          -- Else it was already the latest.
          Version _  ->                                                          -- Expecting a specific version
            if version == installedVersion                                       -- Is the correct version installed?
             then return (Unchanged, installedPkg)
             else do
               upgrade                                                           -- Try upgrading to the correct version.
               get >>= \case
                 Nothing          -> $craftError notFound                        -- Where did it go?
                 Just upgradedPkg -> if version == upgradedPkg ^. pkgVersion     -- Is the correct version installed?
                                      then return (Updated, upgradedPkg)
                                      else $craftError $ wrongVersion (upgradedPkg ^. pkgVersion)


instance Destroyable Package where
  watchDestroy pkg = do
    ce <- ask
    let pm   = ce ^. craftPackageManager
    let name = pkg ^. pkgName
    let get  = (pm ^. pmGetter) name
    get >>= \case
      Nothing -> return (Unchanged, Nothing)
      Just installedPkg -> do
        (pm ^. pmUninstaller) pkg
        get >>= \case
          Nothing            -> return (Removed, Just installedPkg)
          Just unexpectedPkg -> $craftError $ "destroy Package `" ++ name ++ "` failed! " ++ "Found: " ++ show unexpectedPkg


instance Craftable User User where
  watchCraft user = do
    let name = show $ user ^. userName
    let notFound = $craftError $ "User `" ++ name ++ "` not found!"
    userFromStr name >>= \case
      Nothing     -> do
        useradd user
        userFromStr name >>= \case
          Nothing -> notFound
          Just actualUser -> do
            verify user actualUser
            return (Created, actualUser)
      Just existingUser -> do
        res <- mapM (\(test', act) -> if test' user existingUser
                                      then return True
                                      else act >> return False)
                 [ (test userName,            $notImplemented "set username")
                 , (test uid,                 $notImplemented "set uid")
                 , (test (userGroup . groupName), $notImplemented "set group")
                 , (test userGroups,              $notImplemented "set groups")
                 , (test userHome,                $notImplemented "set home")
                 , (test userPasswordHash,        $notImplemented "set passwordHash")
                 , (test userShell,               $notImplemented "set shell")
                 ]
        if and res
        then return (Unchanged, existingUser)
        else
          userFromStr (show name) >>= \case
            Nothing -> notFound
            Just user'' -> do
              verify user'' user
              return (Updated, user'')

   where
    test :: Eq a => Lens' b a -> b -> b -> Bool
    test l a b = a ^. l == b ^. l
    verify :: User -> User -> Craft ()
    verify _expectedUser _actualUser = $notImplemented "verify User"


instance Craftable Group Group where
  watchCraft grp = do
    _ <- $notImplemented "craft Group"
    -- groupFromName . groupname
    exec_ "groupadd" $ toArg "--gid" (grp ^. Craft.Types.gid) ++ [show $ grp ^. groupName]
    exec_ "gpasswd" ["--members", intercalate "," (map show (grp ^. groupMembers))
                             , show $ grp ^. groupName]
    return (Unchanged, grp)


instance Craftable File File where
  watchCraft f = do
    let fp = f ^. path
    -- FIXME: Don't use _Just
    let expectedMD5 = show . md5 . BL.fromStrict $ f ^. fileContent . _Just
    let err :: String -> Craft a
        err str = $craftError $ "craft File `"++show fp++"` failed! "++str
    let verifyMode m =
          when (m /= f ^. mode) $
            err $ "Wrong Mode: " ++ show m ++ " Expected: " ++ show (f ^. mode)
    let verifyOwner o =
          when (o /= f ^. ownerID) $
            err $ "Wrong OwnerID: " ++ show o ++ " Expected: " ++ show (f ^. ownerID)
    let verifyGroup g =
          when (g /= f ^. groupID) $
            err $ "Wrong GroupID: " ++ show g ++ " Expected: " ++ show (f ^. groupID)
    let verifyStats (m, o, g) = verifyMode m >> verifyOwner o >> verifyGroup g
    getStats fp >>= \case
      Nothing          -> do
        case f ^. fileContent of
          Nothing -> exec_ "touch" [fromAbsFile fp]
          Just c  -> File.write fp c
        File.setStats f
        when (isJust $ f  ^. fileContent) $ do
          actualMD5 <- File.md5sum fp
          unless (actualMD5 == expectedMD5) $
            err "Content Mismatch."
        return (Created, f)
      Just (m', o', g') -> do
        let checks = [ (f ^. mode    == m', setMode    (f ^. mode)    fp)
                     , (f ^. ownerID == o', setOwnerID (f ^. ownerID) fp)
                     , (f ^. groupID == g', setGroupID (f ^. groupID) fp)
                     ]
        mapM_ (uncurry unless) checks
        case f ^. fileContent of
          Nothing -> if all fst checks
                     then return (Unchanged, f)
                     else do
                       getStats fp >>= \case
                         Nothing    -> err "Not Found."
                         Just stats -> verifyStats stats
                       return (Updated, f)
          Just c -> do
            actualMD5 <- File.md5sum fp
            if actualMD5 == expectedMD5
            then return $ if all fst checks
                          then (Unchanged, f)
                          else (Updated, f)
            else do
              File.write fp c
              md5AfterWrite <- File.md5sum fp
              if md5AfterWrite == expectedMD5
                then return (Updated, f)
                else err "Content Mismatch."

  craft f = do
    let fp = f^.path
    exec_ "touch" ["-a", fromAbsFile fp]
    case f ^. fileContent of
      Nothing -> return ()
      Just c  -> do
        let expectedMD5 = show . md5 $ BL.fromStrict c
        actualMD5 <- File.md5sum fp
        unless (actualMD5 == expectedMD5) $ do
          File.write fp c
          md5AfterWrite <- File.md5sum fp
          unless (expectedMD5 == md5AfterWrite) $
            $craftError $ "craft File `"++show fp++"` failed! Content Mismatch."
    File.setStats f
    return f


instance Destroyable File where
  watchDestroy f =
    File.get (f ^. path) >>= \case
      Nothing -> return (Unchanged, Nothing)
      Just f' -> do
        destroy_ f
        return (Removed, Just f')

  destroy_ f = do
    let fp = f^.path
    exec_ "rm" ["-f", fromAbsFile fp]
    File.exists fp >>= flip when (
      $craftError $ "destroy File `"++show fp++"` failed! Found.")


instance Craftable Directory Directory where
  watchCraft d = do
    let dp = d ^. path
        setMode'  = setMode (d ^. mode) dp
        setOwner' = setOwnerID (d ^. ownerID) dp
        setGroup' = setGroupID (d ^. groupID) dp
        error' :: String -> Craft a
        error' str = $craftError
           $ formatToString ("craft Directory `"%string%"` failed! "%string)
                            (show dp) str
        verifyMode m =
          when (m /= d ^. mode) $
            error' $ formatToString ("Wrong Mode: "%shown%" Expected: "%shown)
                                    m (d ^. mode)
        verifyOwner o =
          when (o /= d ^. ownerID) $
            error' $ formatToString ("Wrong Owner ID: "%shown%" Expected: "%shown)
                                    o (d ^. ownerID)
        verifyGroup g =
          when (g /= d ^. groupID) $
            error' $ formatToString ("Wrong Group ID: "%shown%" Expected: "%shown)
                                    g (d ^. groupID)
        verifyStats (m, o, g) =
          verifyMode m >> verifyOwner o >> verifyGroup g
    getStats dp >>= \case
      Nothing -> do
        exec_ "mkdir" ["-p", fromAbsDir dp]
        setMode' >> setOwner' >> setGroup'
        getStats dp >>= \case
          Nothing -> error' "Not Found."
          Just stats' -> verifyStats stats' >> return (Created, d)
      Just (m', o', g') -> do
        let checks = [ (d^.mode    == m', setMode')
                     , (d^.ownerID == o', setOwner')
                     , (d^.groupID == g', setGroup')
                     ]
        mapM_ (uncurry unless) checks
        if all fst checks then
          return (Unchanged, d)
        else
          getStats dp >>= \case
            Nothing -> error' "Not Found."
            Just stats' -> verifyStats stats' >> return (Updated, d)