packages feed

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 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