packages feed

forest-fire 0.1.1.1 → 0.2

raw patch · 8 files changed

+172/−57 lines, 8 filesdep +HUnitdep +mtldep +tastydep −pretty-treePVP ok

version bump matches the API change (PVP)

Dependencies added: HUnit, mtl, tasty, tasty-hunit

Dependencies removed: pretty-tree

API changes (from Hackage documentation)

- Lib: findExportsByStack :: StackName -> IO [ExportName]
- Lib: whoImportsThisValue :: ExportName -> IO [StackName]
- TreeUtils: dependencyToTree :: Dependency -> Tree String
- TreeUtils: postorder :: Tree a -> [a]
- Types: Dependency :: StackName -> [Dependency] -> Dependency
- Types: [dDependencies] :: Dependency -> [Dependency]
- Types: [dStackName] :: Dependency -> StackName
- Types: data Dependency
- Types: instance GHC.Show.Show Types.Dependency
+ JSONInstances: class Monad m => AWSExecution m
+ JSONInstances: findExportsByStack :: AWSExecution m => StackName -> m [ExportName]
+ JSONInstances: instance JSONInstances.AWSExecution GHC.Types.IO
+ JSONInstances: whoImportsThisValue :: AWSExecution m => ExportName -> m [StackName]
+ Lib: deletionOrder :: Dependency -> [StackName]
+ Lib: outputDeletionPlan :: String -> IO Dependency
+ Lib: safe :: Dependency -> [StackName]
+ Types: instance GHC.Classes.Eq Types.ExportName
+ Types: instance GHC.Classes.Ord Types.StackName
+ Types: snStackName :: StackName -> String
+ Types: type Dependency = Map StackName [StackName]
- Lib: buildDependencyGraph :: StackName -> IO Dependency
+ Lib: buildDependencyGraph :: AWSExecution m => StackName -> m Dependency

Files

README.md view
@@ -1,4 +1,4 @@-# forest-fire+# forest-fire [![Build Status](https://travis-ci.org/toothbrush/forest-fire.svg?branch=master)](https://travis-ci.org/toothbrush/forest-fire)  This is a little command-line tool with an ill-advised name, to easily tear down CloudFormation stacks which have outputs that other stacks@@ -59,6 +59,22 @@ ```sh forest-fire "kubernetes-dynamic-91acf0ef-lifecycle" --delete ```++# Do you Docker?++Some people don't believe in native executables.  For them, i present the Docker version:++```sh+docker container run --rm \+    -e AWS_SECRET_ACCESS_KEY \+    -e AWS_ACCESS_KEY_ID \+    -e AWS_DEFAULT_REGION \+    paulrb/forest-fire:master yourstack+```++It is hosted on Docker Hub: https://hub.docker.com/r/paulrb/forest-fire +and built with Travis CI: https://travis-ci.org/toothbrush/forest-fire +from this repository.  # Credits 
forest-fire.cabal view
@@ -1,5 +1,5 @@ name:                forest-fire-version:             0.1.1.1+version:             0.2 synopsis:            Recursively delete CloudFormation stacks and their dependants description:   This simple tool will repeatedly query CloudFormation@@ -23,15 +23,13 @@   exposed-modules:     Lib                      , AWSCommands                      , JSONInstances-                     , TreeUtils                      , Types   build-depends:       base >= 4.7 && < 5                      , aeson                      , bytestring+                     , containers                      , process                      , text-                     , containers-                     , pretty-tree   default-language:    Haskell2010  executable forest-fire@@ -48,7 +46,14 @@   hs-source-dirs:      test   main-is:             Spec.hs   build-depends:       base+                     , aeson+                     , bytestring+                     , containers                      , forest-fire+                     , HUnit+                     , mtl+                     , tasty+                     , tasty-hunit   ghc-options:         -threaded -rtsopts -with-rtsopts=-N   default-language:    Haskell2010 
src/AWSCommands.hs view
@@ -12,9 +12,9 @@   case code of     ExitSuccess -> pure (B.pack stdout)     _ -> if allowFail-         then pure (B.pack "")-         else do putStrLn stderr-                 error $ show code+             then pure (B.pack "")+             else do putStrLn stderr+                     error $ show code  jsonForDescribeStacks :: StackName -> IO B.ByteString jsonForDescribeStacks (StackName s) =
src/JSONInstances.hs view
@@ -3,6 +3,7 @@ module JSONInstances where  import Types+import AWSCommands  import Data.Aeson import Data.Aeson.Types    -- that's where Parser comes from@@ -28,3 +29,16 @@ instance FromJSON StackName instance FromJSON StackId instance FromJSON ExportName++class Monad m => AWSExecution m where+  findExportsByStack :: StackName -> m [ExportName]+  whoImportsThisValue :: ExportName -> m [StackName]++instance AWSExecution IO where+  findExportsByStack s = do+    json <- eitherDecode <$> jsonForDescribeStacks s :: IO (Either String Stacks)+    either error (pure . map eName . concatMap sExports . sStacks) json++  whoImportsThisValue e = do+    json <- eitherDecode <$> jsonForListImports e :: IO (Either String Imports)+    either (const (pure [])) (pure . iStackNames) json
src/Lib.hs view
@@ -3,49 +3,48 @@ import Types import JSONInstances import AWSCommands-import TreeUtils -import Data.Tree-import Data.Tree.Pretty-import Data.List-import Data.Maybe-import Data.Aeson+import Data.List (nub, sort, delete)+import Data.Map (Map)+import qualified Data.Map as Map +outputDeletionPlan :: String -> IO Dependency+outputDeletionPlan stackName = do+  putStrLn $ "Retrieving dependencies of " ++ stackName ++ "..."+  dag <- buildDependencyGraph (StackName stackName)+  putStrLn "Done.  Deletion order:\n"+  mapM_ print $ deletionOrder dag+  return dag+ showDeletionPlan :: String -> IO () showDeletionPlan stackName = do-  putStrLn $ "Retrieving dependencies of " ++ stackName ++ "..."-  tree <- buildDependencyGraph (StackName stackName)-  putStrLn "Done.  Delete these stacks in postorder traversal:\n"-  putStrLn $ drawTree (dependencyToTree tree)-  putStrLn "Or, delete manually in this order:\n"-  mapM_ putStrLn $ postorder (dependencyToTree tree)+  dag <- outputDeletionPlan stackName   putStrLn "\nIf you trust this app you can execute:"   putStrLn $ "forest-fire \"" ++ stackName ++ "\" --delete\n"  actuallyDoTheDelete :: String -> IO () actuallyDoTheDelete stackName = do-  putStrLn $ "Retrieving dependencies of " ++ stackName ++ "..."-  tree <- buildDependencyGraph (StackName stackName)+  dag <- outputDeletionPlan stackName   putStrLn "Deleting dependencies and stack..."-  mapM_ (doDeletionWait . StackName) $ postorder (dependencyToTree tree)--findExportsByStack :: StackName -> IO [ExportName]-findExportsByStack s = do-  json <- eitherDecode <$> jsonForDescribeStacks s-  either error (pure . map eName . concatMap sExports . sStacks) json+  mapM_ doDeletionWait $ deletionOrder dag -whoImportsThisValue :: ExportName -> IO [StackName]-whoImportsThisValue e = do-  json <- eitherDecode <$> jsonForListImports e :: IO (Either String Imports)-  either (const (pure [])) (pure . iStackNames) json+deletionOrder :: Dependency -> [StackName]+deletionOrder d | d == Map.empty = []+deletionOrder d | null (safe d) = error "bork bork circular dependencies!"+deletionOrder d = safe d ++ deletionOrder (without (safe d) d)+  where+    without :: [StackName] -> Dependency -> Dependency+    without [] d = d+    without (stack : ss) d = Map.map (delete stack) $ Map.delete stack (without ss d) -buildDependencyGraph :: StackName -> IO Dependency-buildDependencyGraph = buildDependencyGraph' [] -- the empty list is because we haven't yet queried any stacks-  where buildDependencyGraph' :: [StackName] -> StackName -> IO Dependency-        buildDependencyGraph' alreadySeen name = do-          outputs <- findExportsByStack name-          importers <- mapM whoImportsThisValue outputs-          let downstreams = nub $ filter (`notElem` alreadySeen) $ concat importers-          downstreamDeps <- mapM (buildDependencyGraph' (alreadySeen ++ downstreams)) downstreams-          pure $ Dependency name downstreamDeps+safe :: Dependency -> [StackName]+safe d = Map.keys $ Map.filter null d +buildDependencyGraph :: AWSExecution m => StackName -> m Dependency+buildDependencyGraph name = do+    outputs <- findExportsByStack name+    importers <- mapM whoImportsThisValue outputs+    let children = sort $ nub $ concat importers+    downstreamDeps <- mapM buildDependencyGraph children+    pure $ Map.unionsWith (\new old->nub ((++) new old))+                          (downstreamDeps ++ [Map.singleton name children])
− src/TreeUtils.hs
@@ -1,10 +0,0 @@-module TreeUtils where--import Data.Tree-import Types--dependencyToTree :: Dependency -> Tree String-dependencyToTree (Dependency (StackName name) deps) = Node name (map dependencyToTree deps)--postorder :: Tree a -> [a]-postorder (Node label kids) = concatMap postorder kids ++ [label]
src/Types.hs view
@@ -3,9 +3,9 @@ module Types where  import GHC.Generics+import Data.Map (Map) -data Dependency = Dependency {dStackName    :: StackName,-                              dDependencies :: [Dependency]} deriving (Show)+type Dependency = Map StackName [StackName] -- list of nodes with outgoing edge targets  data Stack = Stack {sStackId   :: StackId,                     sStackName :: StackName,@@ -14,7 +14,12 @@ newtype Stacks = Stacks { sStacks :: [Stack]} deriving Show newtype Export = Export { eName :: ExportName} deriving Show -newtype StackName  = StackName  String      deriving (Show, Generic, Eq)+newtype StackName  = StackName  String      deriving (Show, Generic, Eq, Ord) newtype StackId    = StackId    String      deriving (Show, Generic, Eq)-newtype ExportName = ExportName String      deriving (Show, Generic)+newtype ExportName = ExportName String      deriving (Show, Generic, Eq) newtype Imports    = Imports    { iStackNames :: [StackName] } deriving (Show)++-- Don't add this as a record field name above, it messes up the JSON+-- parser (specifically: makes it expect a record instead of just a+-- string).+snStackName (StackName s) = s
test/Spec.hs view
@@ -1,2 +1,88 @@-main :: IO ()-main = putStrLn "Test suite not yet implemented"+{-# LANGUAGE GeneralizedNewtypeDeriving #-}++import Test.Tasty (defaultMain, testGroup)+import Test.Tasty.HUnit (assertEqual, testCase)++import Control.Monad.Identity+import Data.Aeson+import qualified Data.ByteString.Lazy.Char8 as B+import Data.Map (Map)+import qualified Data.Map as Map++import Types+import Lib+import JSONInstances++tests =+  testGroup+    "Unit tests"+    [trivialDepsTree, dagIsntTree+    ,deletionTrivial, deletionDAG+    ,testJSONparsing]++main = defaultMain tests++trivialDepsTree =+  testCase "simple deletion case" $+      assertEqual []+          (Map.fromList [(StackName "a", [StackName "b"])+                        ,(StackName "b", [StackName "c"])+                        ,(StackName "c", [])+                        ])+          (runIdentity $ unTest1 $ buildDependencyGraph1 (StackName "a"))+  where buildDependencyGraph1 = buildDependencyGraph :: StackName -> Test1 Dependency++newtype Test1 a = Test1 { unTest1 :: Identity a }+  deriving (Monad,Functor,Applicative)++instance AWSExecution Test1 where+  findExportsByStack (StackName "a") = return [ExportName "a_exp"]+  findExportsByStack (StackName "b") = return [ExportName "b_exp"]+  findExportsByStack (StackName "c") = return []+  whoImportsThisValue (ExportName "a_exp") = return [StackName "b"]+  whoImportsThisValue (ExportName "b_exp") = return [StackName "c"]++dagIsntTree =+  testCase "pathological DAG case" $+      assertEqual []+          (Map.fromList [ (StackName "a", [StackName "c", StackName "d"])+                        , (StackName "d", [StackName "c"])+                        , (StackName "c", [])+                        ])+          (runIdentity $ unTest2 $ buildDependencyGraph1 (StackName "a"))+  where buildDependencyGraph1 = buildDependencyGraph :: StackName -> Test2 Dependency++newtype Test2 a = Test2 { unTest2 :: Identity a }+  deriving (Monad,Functor,Applicative)++instance AWSExecution Test2 where+  findExportsByStack (StackName "a") = return [ExportName "a_exp"]+  findExportsByStack (StackName "d") = return [ExportName "d_exp"]+  findExportsByStack (StackName "c") = return []+  whoImportsThisValue (ExportName "a_exp") = return [StackName "d", StackName "c"]+  whoImportsThisValue (ExportName "d_exp") = return [StackName "c"]++deletionTrivial =+  testCase "simple deletion order from test 1" $+      assertEqual []+          [StackName "c", StackName "b", StackName "a"]+          (deletionOrder (runIdentity $ unTest1 $ buildDependencyGraph1 (StackName "a")))+  where buildDependencyGraph1 = buildDependencyGraph :: StackName -> Test1 Dependency++deletionDAG =+  testCase "tricky deletion order from test 2" $+      assertEqual []+          [StackName "c", StackName "d", StackName "a"]+          (deletionOrder (runIdentity $ unTest2 $ buildDependencyGraph2 (StackName "a")))+  where buildDependencyGraph2 = buildDependencyGraph :: StackName -> Test2 Dependency+++testJSONparsing =+  testCase "test parsing AWS output" $ do+    out <- stuff+    assertEqual []+      [ExportName "kubernetes-staging-controller-ELBControllerInternalHostedZone"+      ,ExportName "kubernetes-staging-controller-IAMRole"]+      out+    where stuff = do json <- eitherDecode <$> B.readFile "test/aws-output-test.json"+                     either error (pure . map eName . concatMap sExports . sStacks) json