module Stackctl.Spec.Cat ( CatOptions(..) , parseCatOptions , runCat ) where import Stackctl.Prelude import Blammo.Logging.Logger (flushLogger) import Data.Aeson import qualified Data.Aeson.Key as Key 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.Config (HasConfig) import Stackctl.DirectoryOption (HasDirectoryOption(..), unDirectoryOption) 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 parseCatOptions :: Parser CatOptions parseCatOptions = 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 , HasConfig env , HasDirectoryOption env , HasFilterOption env ) => CatOptions -> m () runCat CatOptions {..} = do dir <- unDirectoryOption <$> 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 $ nubOrd $ 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)] , maybe [] ppDescription ssyDescription , [cyan "Template" <> ": " <> green (pack ssyTemplate)] , ppObject "Parameters" parametersYamlKVs ssyParameters , ppList "Capabilities" ppCapabilities ssyCapabilities , ppObject "Tags" tagsYamlKVs ssyTags ] where ppObject :: Text -> (a -> [(Text, Maybe Text)]) -> Maybe a -> [Text] ppObject label f mA = fromMaybe [] $ do kvs <- f <$> mA pure $ [cyan label <> ":"] <> map (\(k, mV) -> " " <> cyan k <> ":" <> maybe "" (\v -> " " <> green v) mV ) kvs ppList :: Text -> (a -> [Text]) -> Maybe a -> [Text] ppList label f = maybe [] (((cyan label <> ":") :) . f) ppDescription d = [cyan "Description" <> ": " <> green (unStackDescription d)] ppCapabilities = map ((" - " <>) . green . toText) parametersYamlKVs :: ParametersYaml -> [(Text, Maybe Text)] parametersYamlKVs = mapMaybe parameterYamlKV . unParametersYaml parameterYamlKV :: ParameterYaml -> Maybe (Text, Maybe Text) parameterYamlKV py = (,) <$> (p ^. parameter_parameterKey) <*> pure (p ^. parameter_parameterValue) where p = unParameterYaml py tagsYamlKVs :: TagsYaml -> [(Text, Maybe Text)] tagsYamlKVs = map (tagKV . unTagYaml) . unTagsYaml tagKV :: Tag -> (Text, Maybe Text) tagKV tg = (tg ^. tag_key, tg ^. tag_value . to Just) 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 (Key.fromText 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 " "