stackctl 1.7.1.0 → 1.7.2.0
raw patch · 8 files changed
+121/−35 lines, 8 filesdep +unix
Dependencies added: unix
Files
- CHANGELOG.md +5/−1
- README.md +6/−5
- src/Stackctl/AWS/CloudFormation.hs +42/−25
- src/Stackctl/CancelHandler.hs +33/−0
- src/Stackctl/Spec/Deploy.hs +10/−1
- stackctl.cabal +4/−1
- test/Stackctl/CancelHandlerSpec.hs +19/−0
- test/Stackctl/Spec/Changes/FormatSpec.hs +2/−2
CHANGELOG.md view
@@ -1,4 +1,8 @@-## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.7.1.0...main)+## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.7.2.0...main)++## [v1.7.2.0](https://github.com/freckle/stackctl/compare/v1.7.1.0...v1.7.2.0)++- Automatically cancel any ongoing update-stack operations on `^C` ## [v1.7.1.0](https://github.com/freckle/stackctl/compare/v1.7.0.0...v1.7.1.0)
README.md view
@@ -13,7 +13,7 @@ Tags. `stackctl` can be used to pretty-print, diff, and deploy these specifications. -[spec]: https://github.com/freckle/stackctl/blob/main/doc/stackctl.1.md#stack-specifications+[spec]: https://freckle.github.io/stackctl/#STACK-SPECIFICATIONS This project also contains a Haskell library for doing the same. @@ -32,10 +32,11 @@ curl -L https://raw.githubusercontent.com/freckle/stackctl/main/install | bash ``` -**NOTE**: some in the community have expressed [concerns][curlsh-bad] about the-security of so-called "curl-sh" installations. We think the argument has been-[pretty well debunked][curlsh-ok], but feel free to use the manual steps-instead.+> [!NOTE]+> Some in the community have expressed [concerns][curlsh-bad] about the+> security of so-called "curl-sh" installations. We think the argument has been+> [pretty well debunked][curlsh-ok], but feel free to use the manual steps+> instead. [curlsh-bad]: https://0x46.net/thoughts/2019/04/27/piping-curl-to-shell/ [curlsh-ok]: https://www.arp242.net/curl-to-sh.html
src/Stackctl/AWS/CloudFormation.hs view
@@ -41,6 +41,7 @@ , awsCloudFormationGetStackNamesMatching , awsCloudFormationGetMostRecentStackEventId , awsCloudFormationDeleteStack+ , awsCloudFormationCancelUpdateStack , awsCloudFormationWait , awsCloudFormationGetTemplate @@ -50,6 +51,7 @@ , changeSetJSON , ChangeSetId (..) , ChangeSetName (..)+ , ChangeSetType (..) , Change (..) , ResourceChange (..) , Replacement (..)@@ -66,6 +68,7 @@ import Stackctl.Prelude +import Amazonka.CloudFormation.CancelUpdateStack import Amazonka.CloudFormation.CreateChangeSet hiding (id) import Amazonka.CloudFormation.DeleteChangeSet import Amazonka.CloudFormation.DeleteStack@@ -87,7 +90,7 @@ , _ServiceError ) import qualified Amazonka.Env as Amazonka-import Amazonka.Waiter (Accept (..))+import Amazonka.Waiter (Accept (..), Wait) import Conduit import Control.Lens ((?~)) import Data.Aeson@@ -211,7 +214,7 @@ let req = newDescribeStackEvents & describeStackEvents_stackName- ?~ unStackName stackName+ ?~ unStackName stackName runConduit $ AWS.paginate req@@ -245,7 +248,7 @@ req = newDescribeStackEvents & describeStackEvents_stackName- ?~ unStackName stackName+ ?~ unStackName stackName -- Events are returned most-recent first, so "last" is "first" here getFirstEventId :: [StackEvent] -> Maybe Text@@ -264,16 +267,21 @@ => StackName -> m StackDeleteResult awsCloudFormationDeleteStack stackName = do- let- deleteReq = newDeleteStack $ unStackName stackName- describeReq =- newDescribeStacks & describeStacks_stackName ?~ unStackName stackName-- AWS.simple deleteReq $ const $ pure ()+ let req = newDeleteStack $ unStackName stackName+ AWS.simple req $ const $ pure () logDebug "Awaiting DeleteStack"- stackDeleteResult <$> AWS.await newStackDeleteComplete describeReq+ stackDeleteResult <$> awaitStack newStackDeleteComplete stackName +awsCloudFormationCancelUpdateStack+ :: (MonadIO m, MonadLogger m, MonadAWS m) => StackName -> m ()+awsCloudFormationCancelUpdateStack stackName = do+ let req = newCancelUpdateStack $ unStackName stackName+ AWS.simple req $ const $ pure ()++ logDebug "Awaiting CancelUpdateStack"+ void $ awaitStack newStackRollbackComplete stackName+ awsCloudFormationWait :: (MonadUnliftIO m, MonadAWS m) => StackName@@ -281,10 +289,8 @@ awsCloudFormationWait stackName = do either stackCreateResult stackUpdateResult <$> race- (AWS.await newStackCreateComplete req)- (AWS.await newStackUpdateComplete req)- where- req = newDescribeStacks & describeStacks_stackName ?~ unStackName stackName+ (awaitStack newStackCreateComplete stackName)+ (awaitStack newStackUpdateComplete stackName) awsCloudFormationGetTemplate :: (MonadIO m, MonadAWS m) => StackName -> m Value@@ -293,7 +299,7 @@ req = newGetTemplate & (getTemplate_stackName ?~ unStackName stackName)- . (getTemplate_templateStage ?~ TemplateStage_Original)+ . (getTemplate_templateStage ?~ TemplateStage_Original) -- If decodeStrict fails, assume it's a String of Yaml. See writeStackSpec. decodeTemplateBody body =@@ -303,6 +309,13 @@ body <- resp ^. getTemplateResponse_templateBody pure $ decodeTemplateBody body +awaitStack+ :: (MonadIO m, MonadAWS m) => Wait DescribeStacks -> StackName -> m Accept+awaitStack waiter stackName =+ AWS.await waiter+ $ newDescribeStacks+ & describeStacks_stackName ?~ unStackName stackName+ makeParameter :: Text -> Maybe Text -> Parameter makeParameter k v = newParameter & (parameter_parameterKey ?~ k) . (parameter_parameterValue .~ v)@@ -330,6 +343,7 @@ { csCreationTime :: UTCTime , csChanges :: Maybe [Change] , csChangeSetName :: ChangeSetName+ , csChangeSetType :: ChangeSetType , csExecutionStatus :: ExecutionStatus , csChangeSetId :: ChangeSetId , csParameters :: Maybe [Parameter]@@ -342,12 +356,14 @@ , csResponse :: DescribeChangeSetResponse } -changeSetFromResponse :: DescribeChangeSetResponse -> Maybe ChangeSet-changeSetFromResponse resp =+changeSetFromResponse+ :: ChangeSetType -> DescribeChangeSetResponse -> Maybe ChangeSet+changeSetFromResponse changeSetType resp = ChangeSet <$> (resp ^. describeChangeSetResponse_creationTime) <*> pure (fmap sortChanges $ resp ^. describeChangeSetResponse_changes) <*> (ChangeSetName <$> resp ^. describeChangeSetResponse_changeSetName)+ <*> pure changeSetType <*> (resp ^. describeChangeSetResponse_executionStatus) <*> (ChangeSetId <$> resp ^. describeChangeSetResponse_changeSetId) <*> pure (resp ^. describeChangeSetResponse_parameters)@@ -401,10 +417,10 @@ let req = newCreateChangeSet (unStackName stackName) (unChangeSetName name) & (createChangeSet_changeSetType ?~ changeSetType)- . (createChangeSet_templateBody ?~ templateBody)- . (createChangeSet_parameters ?~ parameters)- . (createChangeSet_capabilities ?~ capabilities)- . (createChangeSet_tags ?~ tags)+ . (createChangeSet_templateBody ?~ templateBody)+ . (createChangeSet_parameters ?~ parameters)+ . (createChangeSet_capabilities ?~ capabilities)+ . (createChangeSet_tags ?~ tags) logInfo $ "Creating changeset..."@@ -415,16 +431,17 @@ void $ AWS.await newChangeSetCreateComplete $ newDescribeChangeSet csId logInfo "Retrieving changeset..."- cs <- awsCloudFormationDescribeChangeSet $ ChangeSetId csId+ cs <- awsCloudFormationDescribeChangeSet changeSetType $ ChangeSetId csId pure $ cs <$ guard (not $ changeSetFailed cs) awsCloudFormationDescribeChangeSet :: (MonadIO m, MonadAWS m)- => ChangeSetId+ => ChangeSetType+ -> ChangeSetId -> m ChangeSet-awsCloudFormationDescribeChangeSet changeSetId = do+awsCloudFormationDescribeChangeSet changeSetType changeSetId = do let req = newDescribeChangeSet $ unChangeSetId changeSetId- AWS.simple req changeSetFromResponse+ AWS.simple req $ changeSetFromResponse changeSetType sortChanges :: [Change] -> [Change] sortChanges = sortByDependencies changeName changeCausedBy
+ src/Stackctl/CancelHandler.hs view
@@ -0,0 +1,33 @@+module Stackctl.CancelHandler+ ( with+ , install+ , remove+ , trigger+ ) where++import Stackctl.Prelude++import System.Posix.Signals++-- | Install a 'keyboardSignal' handler, run an action, then remove it+with :: MonadUnliftIO m => m a -> m b -> m b+with f = bracket_ (install f) remove++-- | Install a 'keyboardSignal' handler that runs the given action once+install :: MonadUnliftIO m => m a -> m ()+install f = do+ withRunInIO $ \runInIO -> do+ let handler = Catch $ void $ do+ remove -- so next Ctl-C will truly cancel+ runInIO f+ void $ installHandler keyboardSignal handler Nothing++-- | Remove the current handler for 'keyboardSignal' (i.e. install 'Default')+remove :: MonadIO m => m ()+remove = liftIO $ void $ installHandler keyboardSignal Default Nothing++-- | Trigger the installed 'keyboardSignal' handler+--+-- This is used by our test suite.+trigger :: MonadIO m => m ()+trigger = liftIO $ raiseSignal keyboardSignal
src/Stackctl/Spec/Deploy.hs view
@@ -14,6 +14,7 @@ import Stackctl.AWS hiding (action) import Stackctl.AWS.Scope import Stackctl.Action+import qualified Stackctl.CancelHandler as CancelHandler import Stackctl.Colors import Stackctl.Config (HasConfig) import Stackctl.DirectoryOption (HasDirectoryOption)@@ -206,8 +207,16 @@ mLastId <- awsCloudFormationGetMostRecentStackEventId stackName asyncTail <- async $ tailStackEventsSince stackName mLastId + let onCancel = do+ logInfo "Canceling stack update, press ^C again to abort"+ case csChangeSetType changeSet of+ ChangeSetType_UPDATE -> do+ awsCloudFormationCancelUpdateStack stackName+ cancel asyncTail+ t -> logWarn $ "Cannot cancel change-set of this type" :# ["type" .= t]+ logInfo $ "Executing ChangeSet" :# ["changeSetId" .= changeSetId]- result <- do+ result <- CancelHandler.with onCancel $ do awsCloudFormationExecuteChangeSet changeSetId awsCloudFormationWait stackName
stackctl.cabal view
@@ -1,6 +1,6 @@ cabal-version: 1.18 name: stackctl-version: 1.7.1.0+version: 1.7.2.0 license: MIT license-file: LICENSE copyright: 2022 Renaissance Learning Inc@@ -30,6 +30,7 @@ Stackctl.AWS.Orphans Stackctl.AWS.Scope Stackctl.AWS.STS+ Stackctl.CancelHandler Stackctl.CLI Stackctl.ColorOption Stackctl.Colors@@ -120,6 +121,7 @@ time >=1.11.1.1, transformers >=0.5.6.2, typed-process >=0.2.10.1,+ unix >=2.7.2.2, unliftio >=0.2.25.0, unordered-containers >=0.2.19.1, uuid >=1.3.15,@@ -161,6 +163,7 @@ Stackctl.AWS.EC2Spec Stackctl.AWS.LambdaSpec Stackctl.AWS.ScopeSpec+ Stackctl.CancelHandlerSpec Stackctl.Config.RequiredVersionSpec Stackctl.ConfigSpec Stackctl.FilterOptionSpec
+ test/Stackctl/CancelHandlerSpec.hs view
@@ -0,0 +1,19 @@+module Stackctl.CancelHandlerSpec+ ( spec+ ) where++import Stackctl.Prelude++import qualified Stackctl.CancelHandler as CancelHandler+import Test.Hspec++spec :: Spec+spec = do+ describe "with" $ do+ fit "installs a handler for the duration of a block" $ example $ do+ done <- newEmptyMVar++ CancelHandler.install $ putMVar done ()+ CancelHandler.trigger++ takeMVar done `shouldReturn` ()
test/Stackctl/Spec/Changes/FormatSpec.hs view
@@ -6,7 +6,7 @@ import Stackctl.Prelude import Data.Aeson-import Stackctl.AWS.CloudFormation (changeSetFromResponse)+import Stackctl.AWS.CloudFormation (ChangeSetType (..), changeSetFromResponse) import Stackctl.Colors import Stackctl.Spec.Changes.Format import System.FilePath ((-<.>))@@ -28,7 +28,7 @@ formatChangeSetGolden path fmt = do actual <- formatChangeSet noColors OmitFull "some-stack" fmt- . (changeSetFromResponse <=< decodeStrict)+ . (changeSetFromResponse ChangeSetType_UPDATE <=< decodeStrict) . encodeUtf8 <$> readFileUtf8 path