stackctl 1.1.3.1 → 1.1.4.0
raw patch · 8 files changed
+113/−25 lines, 8 files
Files
- CHANGELOG.md +6/−1
- src/Stackctl/AWS/CloudFormation.hs +27/−0
- src/Stackctl/Spec/Capture.hs +42/−19
- src/Stackctl/Spec/Changes.hs +4/−1
- src/Stackctl/Spec/Deploy.hs +4/−1
- src/Stackctl/StackSpec.hs +3/−2
- src/Stackctl/TagOption.hs +25/−0
- stackctl.cabal +2/−1
CHANGELOG.md view
@@ -1,4 +1,9 @@-## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.1.3.1...main)+## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.1.4.0...main)++## [v1.1.4.0](https://github.com/freckle/stackctl/compare/v1.1.3.1...v1.1.4.0)++- Support matching Stacks by glob in `capture`+- Add `--tag` to `changes` and `deploy` ## [v1.1.3.1](https://github.com/freckle/stackctl/compare/v1.1.3.0...v1.1.3.1)
src/Stackctl/AWS/CloudFormation.hs view
@@ -1,5 +1,6 @@ module Stackctl.AWS.CloudFormation ( Stack(..)+ , stack_stackName , stackDescription , stackIsRollbackComplete , StackId(..)@@ -35,6 +36,7 @@ , awsCloudFormationDescribeStackMaybe , awsCloudFormationDescribeStackOutputs , awsCloudFormationDescribeStackEvents+ , awsCloudFormationGetStackNamesMatching , awsCloudFormationGetMostRecentStackEventId , awsCloudFormationDeleteStack , awsCloudFormationWait@@ -70,6 +72,7 @@ import Amazonka.CloudFormation.ExecuteChangeSet import Amazonka.CloudFormation.GetTemplate import Amazonka.CloudFormation.ListChangeSets+import Amazonka.CloudFormation.ListStacks import Amazonka.CloudFormation.Types import qualified Amazonka.CloudFormation.Types.ChangeSetSummary as Summary import Amazonka.CloudFormation.Waiters@@ -96,6 +99,7 @@ import Stackctl.AWS.Core import Stackctl.Sort import Stackctl.StackDescription+import System.FilePath.Glob import UnliftIO.Exception.Lens (handling_, trying) stackDescription :: Stack -> Maybe StackDescription@@ -213,6 +217,22 @@ .| takeWhileC (\e -> Just (e ^. stackEvent_eventId) /= mLastId) .| sinkList +awsCloudFormationGetStackNamesMatching+ :: (MonadResource m, MonadReader env m, HasAwsEnv env)+ => Pattern+ -> m [StackName]+awsCloudFormationGetStackNamesMatching p = do+ let req = newListStacks & listStacks_stackStatusFilter ?~ runningStatuses++ runConduit+ $ awsPaginate req+ .| concatMapC (^. listStacksResponse_stackSummaries)+ .| concatC+ .| mapC (^. stackSummary_stackName)+ .| filterC ((p `match`) . unpack)+ .| mapC StackName+ .| sinkList+ awsCloudFormationGetMostRecentStackEventId :: (MonadResource m, MonadReader env m, HasAwsEnv env) => StackName@@ -462,6 +482,13 @@ stackIsRollbackComplete :: Stack -> Bool stackIsRollbackComplete stack = stack ^. stack_stackStatus == StackStatus_ROLLBACK_COMPLETE++runningStatuses :: [StackStatus]+runningStatuses =+ [ StackStatus_CREATE_COMPLETE+ , StackStatus_UPDATE_COMPLETE+ , StackStatus_UPDATE_ROLLBACK_COMPLETE+ ] _ValidationError :: AsError a => Getting (First ServiceError) a ServiceError _ValidationError =
src/Stackctl/Spec/Capture.hs view
@@ -13,6 +13,7 @@ import Stackctl.DirectoryOption (HasDirectoryOption(..)) import Stackctl.Spec.Generate import Stackctl.StackSpec+import System.FilePath.Glob data CaptureOptions = CaptureOptions { scoAccountName :: Maybe Text@@ -20,7 +21,7 @@ , scoStackPath :: Maybe FilePath , scoDepends :: Maybe [StackName] , scoTemplateFormat :: TemplateFormat- , scoStackName :: StackName+ , scoStackName :: Pattern } -- brittany-disable-next-binding@@ -54,10 +55,10 @@ ( long "no-flip" <> help "Don't flip JSON templates to Yaml" )- <*> (StackName <$> argument str+ <*> strArgument ( metavar "STACK" <> help "Name of deployed Stack to capture"- ))+ ) runCapture :: ( MonadMask m@@ -74,24 +75,46 @@ -> m () runCapture CaptureOptions {..} = do dir <- view directoryOptionL- stack <- awsCloudFormationDescribeStack scoStackName- template <- awsCloudFormationGetTemplate scoStackName let setScopeName scope = maybe scope (\name -> scope { awsAccountName = name }) scoAccountName - void $ local (awsScopeL %~ setScopeName) $ generate Generate- { gOutputDirectory = dir- , gTemplatePath = scoTemplatePath- , gTemplateFormat = scoTemplateFormat- , gStackPath = scoStackPath- , gStackName = scoStackName- , gDescription = stackDescription stack- , gDepends = scoDepends- , gActions = Nothing- , gParameters = parameters stack- , gCapabilities = capabilities stack- , gTags = tags stack- , gTemplateBody = templateBodyFromValue template- }+ generate' stack template path templatePath = do+ void $ local (awsScopeL %~ setScopeName) $ generate Generate+ { gOutputDirectory = dir+ , gTemplatePath = templatePath+ , gTemplateFormat = scoTemplateFormat+ , gStackPath = path+ , gStackName = StackName $ stack ^. stack_stackName+ , gDescription = stackDescription stack+ , gDepends = scoDepends+ , gActions = Nothing+ , gParameters = parameters stack+ , gCapabilities = capabilities stack+ , gTags = tags stack+ , gTemplateBody = templateBodyFromValue template+ }++ results <- awsCloudFormationGetStackNamesMatching scoStackName++ case results of+ [] -> do+ logError+ $ "No Active Stacks match "+ <> pack (decompile scoStackName)+ :# []+ exitFailure++ [stackName] -> do+ stack <- awsCloudFormationDescribeStack stackName+ template <- awsCloudFormationGetTemplate stackName+ generate' stack template scoStackPath scoTemplatePath+ stackNames -> do+ logInfo "Capturing multiple matching Stacks"+ for_ scoStackPath $ \_ -> logWarn "--path option ignored"+ for_ scoTemplatePath $ \_ -> logWarn "--template-path option ignored"+ for_ stackNames $ \stackName -> do+ stack <- awsCloudFormationDescribeStack stackName+ template <- awsCloudFormationGetTemplate stackName+ generate' stack template Nothing Nothing
src/Stackctl/Spec/Changes.hs view
@@ -20,10 +20,12 @@ import Stackctl.Spec.Discover import Stackctl.StackSpec import Stackctl.StackSpecPath+import Stackctl.TagOption data ChangesOptions = ChangesOptions { scoFormat :: Format , scoParameters :: [Parameter]+ , scoTags :: [Tag] , scoOutput :: Maybe FilePath } @@ -33,6 +35,7 @@ runChangesOptions = ChangesOptions <$> formatOption <*> many parameterOption+ <*> many tagOption <*> optional (argument str ( metavar "PATH" <> help "Write changes summary to PATH"@@ -62,7 +65,7 @@ for_ specs $ \spec -> do withThreadContext ["stackName" .= stackSpecStackName spec] $ do- emChangeSet <- createChangeSet spec scoParameters+ emChangeSet <- createChangeSet spec scoParameters scoTags case emChangeSet of Left err -> do
src/Stackctl/Spec/Deploy.hs view
@@ -23,10 +23,12 @@ import Stackctl.Spec.Changes.Format import Stackctl.Spec.Discover import Stackctl.StackSpec+import Stackctl.TagOption import UnliftIO.Directory (createDirectoryIfMissing) data DeployOptions = DeployOptions { sdoParameters :: [Parameter]+ , sdoTags :: [Tag] , sdoSaveChangeSets :: Maybe FilePath , sdoDeployConfirmation :: DeployConfirmation , sdoClean :: Bool@@ -37,6 +39,7 @@ runDeployOptions :: Parser DeployOptions runDeployOptions = DeployOptions <$> many parameterOption+ <*> many tagOption <*> optional (strOption ( long "save-change-sets" <> metavar "DIRECTORY"@@ -74,7 +77,7 @@ withThreadContext ["stackName" .= stackSpecStackName spec] $ do handleRollbackComplete sdoDeployConfirmation $ stackSpecStackName spec - emChangeSet <- createChangeSet spec sdoParameters+ emChangeSet <- createChangeSet spec sdoParameters sdoTags case emChangeSet of Left err -> do
src/Stackctl/StackSpec.hs view
@@ -170,15 +170,16 @@ ) => StackSpec -> [Parameter]+ -> [Tag] -> m (Either Text (Maybe ChangeSet))-createChangeSet spec parameters = awsCloudFormationCreateChangeSet+createChangeSet spec parameters tags = awsCloudFormationCreateChangeSet (stackSpecStackName spec) (stackSpecStackDescription spec) (stackSpecTemplate spec) (nubOrdOn (^. parameter_parameterKey) $ parameters <> stackSpecParameters spec ) (stackSpecCapabilities spec)- (stackSpecTags spec)+ (nubOrdOn (^. tag_key) $ tags <> stackSpecTags spec) sortStackSpecs :: [StackSpec] -> [StackSpec] sortStackSpecs = sortByDependencies stackSpecStackName stackSpecDepends
+ src/Stackctl/TagOption.hs view
@@ -0,0 +1,25 @@+module Stackctl.TagOption+ ( tagOption+ ) where++import Stackctl.Prelude++import qualified Data.Text as T+import Options.Applicative+import Stackctl.AWS.CloudFormation (Tag, newTag)++tagOption :: Parser Tag+tagOption = option (eitherReader readTag) $ mconcat+ [ short 't'+ , long "tag"+ , metavar "KEY=[VALUE]"+ , help "Override the given Tag for this operation"+ ]++readTag :: String -> Either String Tag+readTag s = case T.breakOn "=" t of+ (_, v) | T.null v -> Left $ "No '=' found (" <> s <> ")"+ (k, _) | T.null k -> Left $ "Empty key (" <> s <> ")"+ (k, "=") -> Right $ newTag k ""+ (k, v) -> Right $ newTag k $ T.drop 1 v+ where t = pack s
stackctl.cabal view
@@ -1,6 +1,6 @@ cabal-version: 1.18 name: stackctl-version: 1.1.3.1+version: 1.1.4.0 license: MIT license-file: LICENSE copyright: 2022 Renaissance Learning Inc@@ -54,6 +54,7 @@ Stackctl.StackSpecPath Stackctl.StackSpecYaml Stackctl.Subcommand+ Stackctl.TagOption Stackctl.VerboseOption Stackctl.Version UnliftIO.Exception.Lens