module Stackctl.Spec.Cat ( CatOptions(..) , runCatOptions , runCat ) where import Stackctl.Prelude import Blammo.Logging.Logger (flushLogger) import Data.Aeson import Data.Aeson.Lens import qualified Data.HashMap.Strict as HashMap import Data.List (sort, sortOn) import qualified Data.List.NonEmpty as NE import qualified Data.Text as T import qualified Data.Text.IO as T import qualified Data.Yaml as Yaml import Options.Applicative import Stackctl.AWS import Stackctl.AWS.Scope import Stackctl.Colors import Stackctl.DirectoryOption (HasDirectoryOption(..)) import Stackctl.FilterOption (HasFilterOption) import Stackctl.Spec.Discover import Stackctl.StackSpec import Stackctl.StackSpecPath import Stackctl.StackSpecYaml data CatOptions = CatOptions { sctoNoStacks :: Bool , sctoNoTemplates :: Bool , sctoBrief :: Bool } -- brittany-disable-next-binding runCatOptions :: Parser CatOptions runCatOptions = CatOptions <$> switch ( long "no-stacks" <> help "Only show templates/" ) <*> switch ( long "no-templates" <> help "Only show stacks/" ) <*> switch ( short 'b' <> long "brief" <> help "Don't show file contents, only paths" ) runCat :: ( MonadMask m , MonadResource m , MonadLogger m , MonadReader env m , HasLogger env , HasAwsScope env , HasDirectoryOption env , HasFilterOption env , HasColorOption env ) => CatOptions -> m () runCat CatOptions {..} = do dir <- view directoryOptionL colors@Colors {..} <- getColorsStdout tree <- specTree <$> discoverSpecs let putStack n x = if sctoNoStacks then pure () else put n x putStackBody n x = if sctoNoStacks || sctoBrief then pure () else putBoxed n x putTemplate n x = if sctoNoTemplates then pure () else put n x putTemplateBody n x = if sctoNoTemplates || sctoBrief then pure () else putBoxed n x flushLogger put 0 $ fromString dir <> "/" putStack 2 "stacks/" templates <- for tree $ \((accountId, accountName), regions) -> do putStack 4 $ magenta (unAccountId accountId) <> "." <> accountName <> "/" for regions $ \(region, specs) -> do putStack 6 $ magenta (toText region) <> "/" let sorted = sortOn (stackSpecPathBasePath . stackSpecSpecPath) specs for sorted $ \spec -> do let base = stackSpecPathBasePath $ stackSpecSpecPath spec body = stackSpecSpecBody spec name = stackSpecStackName spec yaml = prettyPrintStackSpecYaml colors name body putStack 8 $ magenta (fromString base) putStackBody 10 yaml pure $ ssyTemplate body putTemplate 2 "templates/" for_ (sort $ concat $ concat templates) $ \template -> do val <- Yaml.decodeFileThrow @_ @Value $ dir "templates" template putTemplate 4 $ green $ fromString template putTemplateBody 6 $ prettyPrintTemplate colors val specTree :: [StackSpec] -> [((AccountId, Text), [(Region, [StackSpec])])] specTree = map (second groupRegion) . groupAccount where groupRegion :: [StackSpec] -> [(Region, [StackSpec])] groupRegion = groupTo (stackSpecPathRegion . stackSpecSpecPath) groupAccount :: [StackSpec] -> [((AccountId, Text), [StackSpec])] groupAccount = groupTo ((stackSpecPathAccountId &&& stackSpecPathAccountName) . stackSpecSpecPath) groupTo :: Ord b => (a -> b) -> [a] -> [(b, [a])] groupTo f = map (f . NE.head &&& NE.toList) . NE.groupAllWith f prettyPrintStackSpecYaml :: Colors -> StackName -> StackSpecYaml -> [Text] prettyPrintStackSpecYaml Colors {..} name StackSpecYaml {..} = concat [ [cyan "Name" <> ": " <> green (unStackName name)] , [cyan "Template" <> ": " <> green (pack ssyTemplate)] , ppList "Parameters" (ppParameters . map unParameterYaml) ssyParameters , ppList "Capabilities" ppCapabilities ssyCapabilities , ppList "Tags" (ppTags . map unTagYaml) ssyTags ] where ppList :: Text -> (a -> [Text]) -> Maybe a -> [Text] ppList label f = maybe [] (((cyan label <> ":") :) . f) ppParameters = concatMap $ \p -> [ " - " <> cyan "ParameterKey" <> ": " <> maybe "" green (p ^. parameter_parameterKey) , " " <> cyan "ParameterValue" <> ": " <> maybe "" toText (p ^. parameter_parameterValue) ] ppCapabilities = map ((" - " <>) . green . toText) ppTags = concatMap $ \tg -> [ " - " <> cyan "Key" <> ": " <> green (tg ^. tag_key) , " " <> cyan "Value" <> ": " <> (tg ^. tag_value) ] prettyPrintTemplate :: Colors -> Value -> [Text] prettyPrintTemplate Colors {..} val = concat [ displayTextProperty "Description" , displayObjectProperty "Parameters" , displayObjectProperty "Resources" , displayObjectProperty "Outputs" ] where displayTextProperty :: Text -> [Text] displayTextProperty = displayPropertyWith $ \v -> let tp = T.dropWhileEnd (== '\n') $ pack v in [" " <> green tp] displayObjectProperty :: Text -> [Text] displayObjectProperty = displayPropertyWith @(HashMap Text Value) $ map ((" - " <>) . green) . sort . HashMap.keys displayPropertyWith :: (FromJSON a, ToJSON a) => (a -> [Text]) -> Text -> [Text] displayPropertyWith f k = cyan k <> ": " : fromMaybe [] displayValue where displayValue = val ^? key k . _JSON . to f putBoxed :: MonadIO m => Int -> [Text] -> m () putBoxed n xs = do traverse_ (put n . ("│ " <>)) xs put n "└──────────" put 0 "" put :: MonadIO m => Int -> Text -> m () put n = liftIO . T.putStrLn . (indent <>) where indent = mconcat $ replicate n " "