From b32c051ed8fe8df4676149acbd3cb942a49266f0 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 13 Dec 2022 11:51:26 -0500 Subject: [PATCH 001/187] Support Description in StackSpecs Descriptions are useful to attach to Stacks, we should support that in our specs. If users put a `Description` in their `templates/`, we have (and will continue to) respect those simply by consequence of CloudFormation doing so, but I think those ergonomics are wrong. If we think of stacks as instantiations of re-used templates, the fact that the description lives as a property on the template instead of the stack is almost certainly a historical accident. What good is describing all of your auto-scaling groups, which make use of a nice parameterized template, as "An Auto-scaling group"? Stackctl affords us the ability to correct for that. We accept `Description` in the stack-specific specs, and (so long as there's no `Description` already in the template being used), we write that in on deployment of that template. Inserting the descriptions in Yaml is done carefully. We do a naive textual insert of a `Description` key at the start of the Yaml, rather than the more robust parse-update-render. The reason is we want to preserve any formatting and comments from the on-disk representation in the in-cloud version. This feels worthwhile. Besides preventing surprise and confusion for anyone looking at the in-cloud representation, it also ensures that creating change sets when there are no changes will remain fast. I have no proof, but AWS _seems_ to leverage some kind of template checksum to make the no-changes case faster. --- doc/stackctl.1.md | 10 ++++++ src/Stackctl/AWS/CloudFormation.hs | 16 ++++++--- src/Stackctl/Spec/Capture.hs | 1 + src/Stackctl/Spec/Generate.hs | 6 ++-- src/Stackctl/StackDescription.hs | 52 +++++++++++++++++++++++++++ src/Stackctl/StackSpec.hs | 7 +++- src/Stackctl/StackSpecYaml.hs | 5 +-- stackctl.cabal | 2 ++ test/Stackctl/StackDescriptionSpec.hs | 42 ++++++++++++++++++++++ test/Stackctl/StackSpecSpec.hs | 3 +- 10 files changed, 133 insertions(+), 11 deletions(-) create mode 100644 src/Stackctl/StackDescription.hs create mode 100644 test/Stackctl/StackDescriptionSpec.hs diff --git a/doc/stackctl.1.md b/doc/stackctl.1.md index cf4c96d..889b5b4 100644 --- a/doc/stackctl.1.md +++ b/doc/stackctl.1.md @@ -92,6 +92,8 @@ Its constituent parts are used as follows: These files' contents should be: ``` +Description: + Template: Depends: @@ -116,6 +118,14 @@ Tags: And these constituent parts are used as follows: +*{.Description}*\ + +> Optional. Set the Stack's description. +> +> This value will be inserted as the *Description* key in the template body on +> deployment, which becomes the deployed Stack's description. If the template +> already contains a description, the specification value will be ignored. + *{.Template}*\ > Required. The template to use when deploying this Stack. Must be a relative diff --git a/src/Stackctl/AWS/CloudFormation.hs b/src/Stackctl/AWS/CloudFormation.hs index 4ef4ca9..9d31d38 100644 --- a/src/Stackctl/AWS/CloudFormation.hs +++ b/src/Stackctl/AWS/CloudFormation.hs @@ -1,10 +1,10 @@ module Stackctl.AWS.CloudFormation - ( - -- * Stacks - Stack(..) + ( Stack(..) + , stackDescription , stackIsRollbackComplete , StackId(..) , StackName(..) + , StackDescription(..) , StackEvent(..) , ResourceStatus(..) , stackEvent_eventId @@ -95,8 +95,12 @@ import qualified Data.UUID as UUID import qualified Data.UUID.V4 as UUID import Stackctl.AWS.Core import Stackctl.Sort +import Stackctl.StackDescription import UnliftIO.Exception.Lens (handling_, trying) +stackDescription :: Stack -> Maybe StackDescription +stackDescription = fmap StackDescription . (^. stack_description) + newtype StackId = StackId { unStackId :: Text } @@ -328,17 +332,19 @@ awsCloudFormationCreateChangeSet , HasAwsEnv env ) => StackName + -> Maybe StackDescription -> StackTemplate -> [Parameter] -> [Capability] -> [Tag] -> m (Either Text (Maybe ChangeSet)) -awsCloudFormationCreateChangeSet stackName stackTemplate parameters capabilities tags +awsCloudFormationCreateChangeSet stackName mStackDescription stackTemplate parameters capabilities tags = fmap (first formatServiceError) $ trying (_ServiceError . hasStatus 400) $ do name <- newChangeSetName - templateBody <- readFileUtf8 $ unStackTemplate stackTemplate + templateBody <- addStackDescription mStackDescription + <$> readFileUtf8 (unStackTemplate stackTemplate) mStack <- awsCloudFormationDescribeStackMaybe stackName let diff --git a/src/Stackctl/Spec/Capture.hs b/src/Stackctl/Spec/Capture.hs index be9fadb..5f2b22a 100644 --- a/src/Stackctl/Spec/Capture.hs +++ b/src/Stackctl/Spec/Capture.hs @@ -85,6 +85,7 @@ runCapture CaptureOptions {..} = do , gTemplateFormat = scoTemplateFormat , gStackPath = scoStackPath , gStackName = scoStackName + , gDescription = stackDescription stack , gDepends = scoDepends , gActions = Nothing , gParameters = parameters stack diff --git a/src/Stackctl/Spec/Generate.hs b/src/Stackctl/Spec/Generate.hs index 148ab83..8767bcb 100644 --- a/src/Stackctl/Spec/Generate.hs +++ b/src/Stackctl/Spec/Generate.hs @@ -6,9 +6,9 @@ module Stackctl.Spec.Generate import Stackctl.Prelude +import Stackctl.Action import Stackctl.AWS import Stackctl.AWS.Scope -import Stackctl.Action import Stackctl.Spec.Discover (buildSpecPath) import Stackctl.StackSpec import Stackctl.StackSpecPath @@ -23,6 +23,7 @@ data Generate = Generate , gStackPath :: Maybe FilePath -- ^ If not given, will use @{stack-name}.yaml@ , gStackName :: StackName + , gDescription :: Maybe StackDescription , gDepends :: Maybe [StackName] , gActions :: Maybe [Action] , gParameters :: Maybe [Parameter] @@ -59,7 +60,8 @@ generate Generate {..} = do let templatePath = fromMaybe defaultTemplatePath gTemplatePath specYaml = StackSpecYaml - { ssyTemplate = templatePath + { ssyDescription = gDescription + , ssyTemplate = templatePath , ssyDepends = gDepends , ssyActions = gActions , ssyParameters = map ParameterYaml <$> gParameters diff --git a/src/Stackctl/StackDescription.hs b/src/Stackctl/StackDescription.hs new file mode 100644 index 0000000..fc95edc --- /dev/null +++ b/src/Stackctl/StackDescription.hs @@ -0,0 +1,52 @@ +module Stackctl.StackDescription + ( StackDescription(..) + , addStackDescription + ) where + +import Stackctl.Prelude + +import Control.Lens ((?~)) +import Data.Aeson (FromJSON, Value(..)) +import qualified Data.Aeson as JSON +import Data.Aeson.Lens +import Data.ByteString.Char8 as BS8 +import qualified Data.Yaml as Yaml + +newtype StackDescription = StackDescription + { unStackDescription :: Text + } + deriving newtype (Eq, Ord, Show, FromJSON, ToJSON) + +data BodyContent + = BodyContentJSON Value + | BodyContentYaml Value + +addStackDescription :: Maybe StackDescription -> Text -> Text +addStackDescription mStackDescription body = fromMaybe body $ do + StackDescription d <- mStackDescription + bc <- getBodyContent bs + decodeUtf8 <$> case bc of + BodyContentJSON v -> updateJSON d bs <$ guard (not $ hasDescription v) + BodyContentYaml v -> updateYaml d bs <$ guard (not $ hasDescription v) + where bs = encodeUtf8 body + +getBodyContent :: ByteString -> Maybe BodyContent +getBodyContent body = asum + [ BodyContentJSON . Object <$> JSON.decodeStrict body + , hush $ BodyContentYaml . Object <$> Yaml.decodeEither' body + ] + +-- Inserting a key is easy to do in Yaml without the parsing round-trip that +-- would strip formatting and comments. But updating a key is hard. To avoid +-- this, we just say that we never clobber existing keys. +hasDescription :: Value -> Bool +hasDescription = isJust . (^? key "Description" . _String) + +-- For JSON, don't worry about preserving formatting; do a proper update. +updateJSON :: Text -> ByteString -> ByteString +updateJSON d = atKey "Description" ?~ String d + +-- For Yaml, insert textually to avoid a round-trip dropping comments or +-- changing whitespace. We rely on 'Show' as a naive escape. +updateYaml :: Text -> ByteString -> ByteString +updateYaml d bs = "Description: " <> BS8.pack (show d) <> "\n" <> bs diff --git a/src/Stackctl/StackSpec.hs b/src/Stackctl/StackSpec.hs index e024b68..f2dc800 100644 --- a/src/Stackctl/StackSpec.hs +++ b/src/Stackctl/StackSpec.hs @@ -3,6 +3,7 @@ module Stackctl.StackSpec , stackSpecSpecPath , stackSpecSpecBody , stackSpecStackName + , stackSpecStackDescription , stackSpecActions , stackSpecParameters , stackSpecCapabilities @@ -23,8 +24,8 @@ import Data.Aeson import qualified Data.ByteString.Lazy as BSL import Data.List.Extra (nubOrdOn) import qualified Data.Yaml as Yaml -import Stackctl.AWS import Stackctl.Action +import Stackctl.AWS import Stackctl.Sort import Stackctl.StackSpecPath import Stackctl.StackSpecYaml @@ -46,6 +47,9 @@ stackSpecSpecBody = ssSpecBody stackSpecStackName :: StackSpec -> StackName stackSpecStackName = stackSpecPathStackName . ssSpecPath +stackSpecStackDescription :: StackSpec -> Maybe StackDescription +stackSpecStackDescription = ssyDescription . ssSpecBody + stackSpecDepends :: StackSpec -> [StackName] stackSpecDepends = fromMaybe [] . ssyDepends . ssSpecBody @@ -143,6 +147,7 @@ createChangeSet -> m (Either Text (Maybe ChangeSet)) createChangeSet spec parameters = awsCloudFormationCreateChangeSet (stackSpecStackName spec) + (stackSpecStackDescription spec) (stackSpecTemplateFile spec) (nubOrdOn (^. parameter_parameterKey) $ parameters <> stackSpecParameters spec ) diff --git a/src/Stackctl/StackSpecYaml.hs b/src/Stackctl/StackSpecYaml.hs index 85085f7..f51080d 100644 --- a/src/Stackctl/StackSpecYaml.hs +++ b/src/Stackctl/StackSpecYaml.hs @@ -29,11 +29,12 @@ import Stackctl.Prelude import Data.Aeson import Data.Aeson.Casing import qualified Data.Text as T -import Stackctl.AWS import Stackctl.Action +import Stackctl.AWS data StackSpecYaml = StackSpecYaml - { ssyTemplate :: FilePath + { ssyDescription :: Maybe StackDescription + , ssyTemplate :: FilePath , ssyDepends :: Maybe [StackName] , ssyActions :: Maybe [Action] , ssyParameters :: Maybe [ParameterYaml] diff --git a/stackctl.cabal b/stackctl.cabal index 8d7ef3b..689f4b7 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -52,6 +52,7 @@ library Stackctl.Spec.Deploy Stackctl.Spec.Discover Stackctl.Spec.Generate + Stackctl.StackDescription Stackctl.StackSpec Stackctl.StackSpecPath Stackctl.StackSpecYaml @@ -170,6 +171,7 @@ test-suite spec other-modules: Stackctl.AWS.CloudFormationSpec Stackctl.FilterOptionSpec + Stackctl.StackDescriptionSpec Stackctl.StackSpecSpec Stackctl.StackSpecYamlSpec Paths_stackctl diff --git a/test/Stackctl/StackDescriptionSpec.hs b/test/Stackctl/StackDescriptionSpec.hs new file mode 100644 index 0000000..fa6d383 --- /dev/null +++ b/test/Stackctl/StackDescriptionSpec.hs @@ -0,0 +1,42 @@ +module Stackctl.StackDescriptionSpec + ( spec + ) where + +import Stackctl.Prelude + +import Stackctl.StackDescription +import Test.Hspec + +spec :: Spec +spec = do + describe "addStackDescription" $ do + let aDescription = Just $ StackDescription "A \"cool\" description" + + it "does nothing with nothing" $ do + addStackDescription Nothing "hi there" `shouldBe` "hi there" + + it "does nothing invalid inputs" $ do + for_ ["", "hi there", "[a list]", "{\"invalid\":", "true", "42"] + $ \input -> addStackDescription aDescription input `shouldBe` input + + context "Yaml" $ do + it "adds a Description" $ do + addStackDescription aDescription "Resources: []\n" + `shouldBe` "Description: \"A \\\"cool\\\" description\"\nResources: []\n" + + it "does not clobber or duplicate an existing Description" $ do + addStackDescription + aDescription + "Resources: []\nDescription: Existing description\n" + `shouldBe` "Resources: []\nDescription: Existing description\n" + + context "JSON" $ do + it "adds a Description" $ do + addStackDescription aDescription "{\"Resources\":[]}" + `shouldBe` "{\"Description\":\"A \\\"cool\\\" description\",\"Resources\":[]}" + + it "does not clobber or duplicate an existing Description" $ do + addStackDescription + aDescription + "{\"Resources\":[],\"Description\":\"Existing description\"}" + `shouldBe` "{\"Resources\":[],\"Description\":\"Existing description\"}" diff --git a/test/Stackctl/StackSpecSpec.hs b/test/Stackctl/StackSpecSpec.hs index 7ae366a..7ceaf95 100644 --- a/test/Stackctl/StackSpecSpec.hs +++ b/test/Stackctl/StackSpecSpec.hs @@ -32,7 +32,8 @@ toSpec name depends = buildStackSpec "." specPath specBody stackName = StackName name specPath = stackSpecPath scope stackName "a/b.yaml" specBody = StackSpecYaml - { ssyDepends = Just $ map StackName depends + { ssyDescription = Nothing + , ssyDepends = Just $ map StackName depends , ssyActions = Nothing , ssyTemplate = "" , ssyParameters = Nothing From 6023b1b6f9f9d96577c56b19544b96b705b3faa5 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 13 Dec 2022 19:33:40 -0500 Subject: [PATCH 002/187] Use dev restylers --- .restyled.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.restyled.yaml b/.restyled.yaml index cbd47d1..dbf806a 100644 --- a/.restyled.yaml +++ b/.restyled.yaml @@ -1,3 +1,4 @@ +restylers_version: dev restylers: - brittany - prettier-markdown: From ed45c5a3cdfe3339fa65198af3f9335f22e5ad0d Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 13 Dec 2022 19:36:31 -0500 Subject: [PATCH 003/187] Version bump --- CHANGELOG.md | 6 +++++- package.yaml | 2 +- stackctl.cabal | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf6c439..2111a97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ -## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.1.2.1...main) +## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.1.2.2...main) + +## [v1.1.2.2](https://github.com/freckle/stackctl/compare/v1.1.2.1...v1.1.2.2) + +- Add support for Stack descriptions ## [v1.1.2.1](https://github.com/freckle/stackctl/compare/v1.1.2.0...v1.1.2.1) diff --git a/package.yaml b/package.yaml index 4af5c84..1d9c5b7 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: stackctl -version: 1.1.2.1 +version: 1.1.2.2 github: freckle/stackctl license: MIT author: Freckle Engineering diff --git a/stackctl.cabal b/stackctl.cabal index 689f4b7..94ff43f 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -5,7 +5,7 @@ cabal-version: 1.18 -- see: https://github.com/sol/hpack name: stackctl -version: 1.1.2.1 +version: 1.1.2.2 description: Please see homepage: https://github.com/freckle/stackctl#readme bug-reports: https://github.com/freckle/stackctl/issues From a3ef61a36d1f697234059dce6c836b3059685376 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 13 Dec 2022 19:59:42 -0500 Subject: [PATCH 004/187] Strengthen ParameterYaml type Because `ToJSON` on `ParameterYaml` had to deal with a potentially null key in the underlying `Parameter`, we used `object []` for the case of either side being missing. Having a Key but not a Value is actually a realistic use-case, and we were generating: ```yaml Parameters: - {} ``` When we should've generated: ```yaml Parameters: - ParameterKey: Something ParameterValue: null ``` Or: ```yaml Parameters: - ParameterKey: Something ``` To make this easier to reason about, we upgraded `ParameterYaml` to `data` so we could hold a non-null key and possibly-null value. We wrote `unParameterYaml` to behave the same and a `parameterYaml` to fail _on construction_ for the invalid case of a missing key. This makes it more natural to handle a missing value correctly. --- src/Stackctl/Spec/Generate.hs | 2 +- src/Stackctl/StackSpecYaml.hs | 30 +++++++++++++++++++----------- test/Stackctl/StackSpecYamlSpec.hs | 29 +++++++++++++++++++++++++---- 3 files changed, 45 insertions(+), 16 deletions(-) diff --git a/src/Stackctl/Spec/Generate.hs b/src/Stackctl/Spec/Generate.hs index 8767bcb..e48e321 100644 --- a/src/Stackctl/Spec/Generate.hs +++ b/src/Stackctl/Spec/Generate.hs @@ -64,7 +64,7 @@ generate Generate {..} = do , ssyTemplate = templatePath , ssyDepends = gDepends , ssyActions = gActions - , ssyParameters = map ParameterYaml <$> gParameters + , ssyParameters = mapMaybe parameterYaml <$> gParameters , ssyCapabilities = gCapabilities , ssyTags = map TagYaml <$> gTags } diff --git a/src/Stackctl/StackSpecYaml.hs b/src/Stackctl/StackSpecYaml.hs index f51080d..ee7ae4d 100644 --- a/src/Stackctl/StackSpecYaml.hs +++ b/src/Stackctl/StackSpecYaml.hs @@ -20,7 +20,9 @@ -- module Stackctl.StackSpecYaml ( StackSpecYaml(..) - , ParameterYaml(..) + , ParameterYaml + , parameterYaml + , unParameterYaml , TagYaml(..) ) where @@ -50,16 +52,24 @@ instance ToJSON StackSpecYaml where toJSON = genericToJSON $ aesonPrefix id toEncoding = genericToEncoding $ aesonPrefix id -newtype ParameterYaml = ParameterYaml - { unParameterYaml :: Parameter +data ParameterYaml = ParameterYaml + { _pyKey :: Text + , _pyValue :: Maybe Text } +parameterYaml :: Parameter -> Maybe ParameterYaml +parameterYaml p = do + k <- p ^. parameter_parameterKey + pure $ ParameterYaml k $ p ^. parameter_parameterKey + +unParameterYaml :: ParameterYaml -> Parameter +unParameterYaml (ParameterYaml k v) = makeParameter k v + instance FromJSON ParameterYaml where parseJSON = withObject "Parameter" $ \o -> - (build <$> o .: "Name" <*> o .: "Value") - <|> (build <$> o .: "ParameterKey" <*> o .: "ParameterValue") - where - build k v = ParameterYaml $ makeParameter k $ Just $ unParameterValue v + (build <$> o .: "Name" <*> o .:? "Value") + <|> (build <$> o .: "ParameterKey" <*> o .:? "ParameterValue") + where build k v = ParameterYaml k $ unParameterValue <$> v newtype ParameterValue = ParameterValue { unParameterValue :: Text @@ -76,10 +86,8 @@ instance ToJSON ParameterYaml where toEncoding = pairs . mconcat . parameterPairs parameterPairs :: KeyValue a => ParameterYaml -> [a] -parameterPairs (ParameterYaml p) = fromMaybe [] $ do - k <- p ^. parameter_parameterKey - v <- p ^. parameter_parameterValue - pure ["ParameterKey" .= k, "ParameterValue" .= v] +parameterPairs (ParameterYaml k v) = + ["ParameterKey" .= k, "ParameterValue" .= v] newtype TagYaml = TagYaml { unTagYaml :: Tag diff --git a/test/Stackctl/StackSpecYamlSpec.hs b/test/Stackctl/StackSpecYamlSpec.hs index 212984b..0604ec5 100644 --- a/test/Stackctl/StackSpecYamlSpec.hs +++ b/test/Stackctl/StackSpecYamlSpec.hs @@ -22,7 +22,7 @@ spec = do , " ParameterValue: Bar\n" ] - let Just [ParameterYaml param] = ssyParameters + let Just [param] = map unParameterYaml <$> ssyParameters param ^. parameter_parameterKey `shouldBe` Just "Foo" param ^. parameter_parameterValue `shouldBe` Just "Bar" @@ -34,7 +34,7 @@ spec = do , " ParameterValue: 80\n" ] - let Just [ParameterYaml param] = ssyParameters + let Just [param] = map unParameterYaml <$> ssyParameters param ^. parameter_parameterKey `shouldBe` Just "Port" param ^. parameter_parameterValue `shouldBe` Just "80" @@ -46,7 +46,7 @@ spec = do , " ParameterValue: 3.14\n" ] - let Just [ParameterYaml param] = ssyParameters + let Just [param] = map unParameterYaml <$> ssyParameters param ^. parameter_parameterKey `shouldBe` Just "Pie" param ^. parameter_parameterValue `shouldBe` Just "3.14" @@ -62,6 +62,26 @@ spec = do show ex `shouldBe` "AesonException \"Error in $.Parameters[0].ParameterValue: Expected String or Number, got: Bool False\"" + it "handles null Value" $ do + StackSpecYaml {..} <- Yaml.decodeThrow $ mconcat + [ "Template: foo.yaml\n" + , "Parameters:\n" + , " - ParameterKey: Foo\n" + , " ParameterValue: null\n" + ] + + let Just [param] = map unParameterYaml <$> ssyParameters + param ^. parameter_parameterKey `shouldBe` Just "Foo" + param ^. parameter_parameterValue `shouldBe` Nothing + + it "handles missing Value" $ do + StackSpecYaml {..} <- Yaml.decodeThrow $ mconcat + ["Template: foo.yaml\n", "Parameters:\n", " - ParameterKey: Foo\n"] + + let Just [param] = map unParameterYaml <$> ssyParameters + param ^. parameter_parameterKey `shouldBe` Just "Foo" + param ^. parameter_parameterValue `shouldBe` Nothing + it "also accepts CloudGenesis formatted values" $ do StackSpecYaml {..} <- Yaml.decodeThrow $ mconcat [ "Template: foo.yaml\n" @@ -70,6 +90,7 @@ spec = do , " Value: Bar\n" ] - let Just [ParameterYaml param] = ssyParameters + let Just [param] = map unParameterYaml <$> ssyParameters param ^. parameter_parameterKey `shouldBe` Just "Foo" param ^. parameter_parameterValue `shouldBe` Just "Bar" + From a6f8f8d667c3734ef18c247be9a39c61d5dd17fb Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 13 Dec 2022 20:06:31 -0500 Subject: [PATCH 005/187] Simplify ParameterYaml parsing Better to retain a clearer type in the record, and make it `Text` at the edges. --- src/Stackctl/StackSpecYaml.hs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Stackctl/StackSpecYaml.hs b/src/Stackctl/StackSpecYaml.hs index ee7ae4d..23fa0dd 100644 --- a/src/Stackctl/StackSpecYaml.hs +++ b/src/Stackctl/StackSpecYaml.hs @@ -54,26 +54,26 @@ instance ToJSON StackSpecYaml where data ParameterYaml = ParameterYaml { _pyKey :: Text - , _pyValue :: Maybe Text + , _pyValue :: Maybe ParameterValue } parameterYaml :: Parameter -> Maybe ParameterYaml parameterYaml p = do k <- p ^. parameter_parameterKey - pure $ ParameterYaml k $ p ^. parameter_parameterKey + pure $ ParameterYaml k $ ParameterValue <$> p ^. parameter_parameterKey unParameterYaml :: ParameterYaml -> Parameter -unParameterYaml (ParameterYaml k v) = makeParameter k v +unParameterYaml (ParameterYaml k v) = makeParameter k $ unParameterValue <$> v instance FromJSON ParameterYaml where parseJSON = withObject "Parameter" $ \o -> - (build <$> o .: "Name" <*> o .:? "Value") - <|> (build <$> o .: "ParameterKey" <*> o .:? "ParameterValue") - where build k v = ParameterYaml k $ unParameterValue <$> v + (ParameterYaml <$> o .: "Name" <*> o .:? "Value") + <|> (ParameterYaml <$> o .: "ParameterKey" <*> o .:? "ParameterValue") newtype ParameterValue = ParameterValue { unParameterValue :: Text } + deriving newtype ToJSON instance FromJSON ParameterValue where parseJSON = \case From 7300fb703d7562828df6626cb9e415cfec778114 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 13 Dec 2022 20:36:21 -0500 Subject: [PATCH 006/187] Support more natural syntax for Parameters Originally, we used `ParameterKey`/`Value` to keep the simplest `FromJSON` possible. Since then, we've implemented custom parsing to correctly handle mixed-typed values and even read CloudGenesis' format. Seems silly to continue to make our users spell things out in such a cumbersome syntax. This commit allows for the more natural, ```yaml Parameters: Foo: Bar Baz: Bat ``` Similar treatment for `Tags` is soon to follow. --- src/Stackctl/Spec/Cat.hs | 5 ++++- src/Stackctl/Spec/Generate.hs | 2 +- src/Stackctl/StackSpec.hs | 2 +- src/Stackctl/StackSpecYaml.hs | 32 +++++++++++++++++++++++++++- test/Stackctl/StackSpecYamlSpec.hs | 34 ++++++++++++++++++++++++------ 5 files changed, 65 insertions(+), 10 deletions(-) diff --git a/src/Stackctl/Spec/Cat.hs b/src/Stackctl/Spec/Cat.hs index a8d098c..6c5f07d 100644 --- a/src/Stackctl/Spec/Cat.hs +++ b/src/Stackctl/Spec/Cat.hs @@ -123,7 +123,10 @@ 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 + "Parameters" + (ppParameters . map unParameterYaml . unParametersYaml) + ssyParameters , ppList "Capabilities" ppCapabilities ssyCapabilities , ppList "Tags" (ppTags . map unTagYaml) ssyTags ] diff --git a/src/Stackctl/Spec/Generate.hs b/src/Stackctl/Spec/Generate.hs index e48e321..9789e4b 100644 --- a/src/Stackctl/Spec/Generate.hs +++ b/src/Stackctl/Spec/Generate.hs @@ -64,7 +64,7 @@ generate Generate {..} = do , ssyTemplate = templatePath , ssyDepends = gDepends , ssyActions = gActions - , ssyParameters = mapMaybe parameterYaml <$> gParameters + , ssyParameters = parametersYaml . mapMaybe parameterYaml <$> gParameters , ssyCapabilities = gCapabilities , ssyTags = map TagYaml <$> gTags } diff --git a/src/Stackctl/StackSpec.hs b/src/Stackctl/StackSpec.hs index f2dc800..6ea769f 100644 --- a/src/Stackctl/StackSpec.hs +++ b/src/Stackctl/StackSpec.hs @@ -62,7 +62,7 @@ stackSpecTemplateFile StackSpec {..} = stackSpecParameters :: StackSpec -> [Parameter] stackSpecParameters = - maybe [] (map unParameterYaml) . ssyParameters . ssSpecBody + maybe [] (map unParameterYaml . unParametersYaml) . ssyParameters . ssSpecBody stackSpecCapabilities :: StackSpec -> [Capability] stackSpecCapabilities = fromMaybe [] . ssyCapabilities . ssSpecBody diff --git a/src/Stackctl/StackSpecYaml.hs b/src/Stackctl/StackSpecYaml.hs index 23fa0dd..d237234 100644 --- a/src/Stackctl/StackSpecYaml.hs +++ b/src/Stackctl/StackSpecYaml.hs @@ -20,6 +20,9 @@ -- module Stackctl.StackSpecYaml ( StackSpecYaml(..) + , ParametersYaml + , parametersYaml + , unParametersYaml , ParameterYaml , parameterYaml , unParameterYaml @@ -30,6 +33,9 @@ import Stackctl.Prelude import Data.Aeson import Data.Aeson.Casing +import qualified Data.Aeson.Key as Key +import qualified Data.Aeson.KeyMap as KeyMap +import Data.Aeson.Types (typeMismatch) import qualified Data.Text as T import Stackctl.Action import Stackctl.AWS @@ -39,7 +45,7 @@ data StackSpecYaml = StackSpecYaml , ssyTemplate :: FilePath , ssyDepends :: Maybe [StackName] , ssyActions :: Maybe [Action] - , ssyParameters :: Maybe [ParameterYaml] + , ssyParameters :: Maybe ParametersYaml , ssyCapabilities :: Maybe [Capability] , ssyTags :: Maybe [TagYaml] } @@ -52,6 +58,30 @@ instance ToJSON StackSpecYaml where toJSON = genericToJSON $ aesonPrefix id toEncoding = genericToEncoding $ aesonPrefix id +newtype ParametersYaml = ParametersYaml + { unParametersYaml :: [ParameterYaml] + } + deriving newtype ToJSON + +instance FromJSON ParametersYaml where + parseJSON = \case + Object o -> do + -- NB. There are simpler ways to do this, but making sure we construct + -- things such that we use (.:) to read the value from each key means that + -- error messages will include "Parameters.{k}". See specs for an example. + let parseKey k = ParameterYaml (Key.toText k) <$> o .: k + ParametersYaml <$> traverse parseKey (KeyMap.keys o) + v@Array{} -> ParametersYaml <$> parseJSON v + v -> typeMismatch err v + where + err = + "Object" + <> ", list of {ParameterKey, ParameterValue} Objects" + <> ", or list of {Key, Value} Objects" + +parametersYaml :: [ParameterYaml] -> ParametersYaml +parametersYaml = ParametersYaml + data ParameterYaml = ParameterYaml { _pyKey :: Text , _pyValue :: Maybe ParameterValue diff --git a/test/Stackctl/StackSpecYamlSpec.hs b/test/Stackctl/StackSpecYamlSpec.hs index 0604ec5..fc9798d 100644 --- a/test/Stackctl/StackSpecYamlSpec.hs +++ b/test/Stackctl/StackSpecYamlSpec.hs @@ -22,7 +22,8 @@ spec = do , " ParameterValue: Bar\n" ] - let Just [param] = map unParameterYaml <$> ssyParameters + let + Just [param] = map unParameterYaml . unParametersYaml <$> ssyParameters param ^. parameter_parameterKey `shouldBe` Just "Foo" param ^. parameter_parameterValue `shouldBe` Just "Bar" @@ -34,7 +35,8 @@ spec = do , " ParameterValue: 80\n" ] - let Just [param] = map unParameterYaml <$> ssyParameters + let + Just [param] = map unParameterYaml . unParametersYaml <$> ssyParameters param ^. parameter_parameterKey `shouldBe` Just "Port" param ^. parameter_parameterValue `shouldBe` Just "80" @@ -46,7 +48,8 @@ spec = do , " ParameterValue: 3.14\n" ] - let Just [param] = map unParameterYaml <$> ssyParameters + let + Just [param] = map unParameterYaml . unParametersYaml <$> ssyParameters param ^. parameter_parameterKey `shouldBe` Just "Pie" param ^. parameter_parameterValue `shouldBe` Just "3.14" @@ -62,6 +65,14 @@ spec = do show ex `shouldBe` "AesonException \"Error in $.Parameters[0].ParameterValue: Expected String or Number, got: Bool False\"" + it "has informative errors in Object form" $ do + let + Left ex = Yaml.decodeEither' @StackSpecYaml $ mconcat + ["Template: foo.yaml\n", "Parameters:\n", " Norway: no\n"] + + show ex + `shouldBe` "AesonException \"Error in $.Parameters.Norway: Expected String or Number, got: Bool False\"" + it "handles null Value" $ do StackSpecYaml {..} <- Yaml.decodeThrow $ mconcat [ "Template: foo.yaml\n" @@ -70,7 +81,8 @@ spec = do , " ParameterValue: null\n" ] - let Just [param] = map unParameterYaml <$> ssyParameters + let + Just [param] = map unParameterYaml . unParametersYaml <$> ssyParameters param ^. parameter_parameterKey `shouldBe` Just "Foo" param ^. parameter_parameterValue `shouldBe` Nothing @@ -78,7 +90,8 @@ spec = do StackSpecYaml {..} <- Yaml.decodeThrow $ mconcat ["Template: foo.yaml\n", "Parameters:\n", " - ParameterKey: Foo\n"] - let Just [param] = map unParameterYaml <$> ssyParameters + let + Just [param] = map unParameterYaml . unParametersYaml <$> ssyParameters param ^. parameter_parameterKey `shouldBe` Just "Foo" param ^. parameter_parameterValue `shouldBe` Nothing @@ -90,7 +103,16 @@ spec = do , " Value: Bar\n" ] - let Just [param] = map unParameterYaml <$> ssyParameters + let + Just [param] = map unParameterYaml . unParametersYaml <$> ssyParameters param ^. parameter_parameterKey `shouldBe` Just "Foo" param ^. parameter_parameterValue `shouldBe` Just "Bar" + it "also accepts objects" $ do + StackSpecYaml {..} <- Yaml.decodeThrow + $ mconcat ["Template: foo.yaml\n", "Parameters:\n", " Foo: Bar\n"] + + let + Just [param] = map unParameterYaml . unParametersYaml <$> ssyParameters + param ^. parameter_parameterKey `shouldBe` Just "Foo" + param ^. parameter_parameterValue `shouldBe` Just "Bar" From f164c3f34ce5c91917656ace65f04d2a8a4a5d0a Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 13 Dec 2022 20:43:02 -0500 Subject: [PATCH 007/187] Accept simpler object syntax for Tags See previous commit, which did the same for Parameters, for more details. --- src/Stackctl/Spec/Cat.hs | 2 +- src/Stackctl/Spec/Generate.hs | 2 +- src/Stackctl/StackSpec.hs | 2 +- src/Stackctl/StackSpecYaml.hs | 28 +++++++++++++++++++++++++++- 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/Stackctl/Spec/Cat.hs b/src/Stackctl/Spec/Cat.hs index 6c5f07d..d81ec06 100644 --- a/src/Stackctl/Spec/Cat.hs +++ b/src/Stackctl/Spec/Cat.hs @@ -128,7 +128,7 @@ prettyPrintStackSpecYaml Colors {..} name StackSpecYaml {..} = concat (ppParameters . map unParameterYaml . unParametersYaml) ssyParameters , ppList "Capabilities" ppCapabilities ssyCapabilities - , ppList "Tags" (ppTags . map unTagYaml) ssyTags + , ppList "Tags" (ppTags . map unTagYaml . unTagsYaml) ssyTags ] where ppList :: Text -> (a -> [Text]) -> Maybe a -> [Text] diff --git a/src/Stackctl/Spec/Generate.hs b/src/Stackctl/Spec/Generate.hs index 9789e4b..d3e73fe 100644 --- a/src/Stackctl/Spec/Generate.hs +++ b/src/Stackctl/Spec/Generate.hs @@ -66,7 +66,7 @@ generate Generate {..} = do , ssyActions = gActions , ssyParameters = parametersYaml . mapMaybe parameterYaml <$> gParameters , ssyCapabilities = gCapabilities - , ssyTags = map TagYaml <$> gTags + , ssyTags = tagsYaml . map TagYaml <$> gTags } stackSpec = buildStackSpec gOutputDirectory specPath specYaml diff --git a/src/Stackctl/StackSpec.hs b/src/Stackctl/StackSpec.hs index 6ea769f..fe7a0a5 100644 --- a/src/Stackctl/StackSpec.hs +++ b/src/Stackctl/StackSpec.hs @@ -68,7 +68,7 @@ stackSpecCapabilities :: StackSpec -> [Capability] stackSpecCapabilities = fromMaybe [] . ssyCapabilities . ssSpecBody stackSpecTags :: StackSpec -> [Tag] -stackSpecTags = maybe [] (map unTagYaml) . ssyTags . ssSpecBody +stackSpecTags = maybe [] (map unTagYaml . unTagsYaml) . ssyTags . ssSpecBody buildStackSpec :: FilePath -> StackSpecPath -> StackSpecYaml -> StackSpec buildStackSpec = StackSpec diff --git a/src/Stackctl/StackSpecYaml.hs b/src/Stackctl/StackSpecYaml.hs index d237234..b656330 100644 --- a/src/Stackctl/StackSpecYaml.hs +++ b/src/Stackctl/StackSpecYaml.hs @@ -26,6 +26,9 @@ module Stackctl.StackSpecYaml , ParameterYaml , parameterYaml , unParameterYaml + , TagsYaml + , tagsYaml + , unTagsYaml , TagYaml(..) ) where @@ -47,7 +50,7 @@ data StackSpecYaml = StackSpecYaml , ssyActions :: Maybe [Action] , ssyParameters :: Maybe ParametersYaml , ssyCapabilities :: Maybe [Capability] - , ssyTags :: Maybe [TagYaml] + , ssyTags :: Maybe TagsYaml } deriving stock Generic @@ -119,6 +122,29 @@ parameterPairs :: KeyValue a => ParameterYaml -> [a] parameterPairs (ParameterYaml k v) = ["ParameterKey" .= k, "ParameterValue" .= v] +newtype TagsYaml = TagsYaml + { unTagsYaml :: [TagYaml] + } + deriving newtype ToJSON + +instance FromJSON TagsYaml where + parseJSON = \case + Object o -> do + -- NB. There are simpler ways to do this, but making sure we construct + -- things such that we use (.:) to read the value from each key means that + -- error messages will include "Parameters.{k}". See specs for an example. + let + parseKey k = do + t <- newTag (Key.toText k) <$> o .: k + pure $ TagYaml t + TagsYaml <$> traverse parseKey (KeyMap.keys o) + v@Array{} -> TagsYaml <$> parseJSON v + v -> typeMismatch err v + where err = "Object or list of {Key, Value} Objects" + +tagsYaml :: [TagYaml] -> TagsYaml +tagsYaml = TagsYaml + newtype TagYaml = TagYaml { unTagYaml :: Tag } From 78bfca931b2853c1cec1c1c66cb4c9faabac3fbf Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 13 Dec 2022 20:50:50 -0500 Subject: [PATCH 008/187] Document natural formats for Parameters/Tags --- doc/stackctl.1.md | 47 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/doc/stackctl.1.md b/doc/stackctl.1.md index 889b5b4..e85a7dd 100644 --- a/doc/stackctl.1.md +++ b/doc/stackctl.1.md @@ -104,16 +104,12 @@ Actions: run: : -Parameters: - - ParameterKey: - ParameterValue: +Parameters: Object Capabilities: - -Tags: - - Key: - Value: +Tags: Object ``` And these constituent parts are used as follows: @@ -157,6 +153,29 @@ And these constituent parts are used as follows: *{.Parameters}*\ > Optional. Parameters to use when deploying the Stack. +> +> The *Parameters* key can be specified in any of 3 forms: +> +> ``` +> # Natural (recommended) +> Parameters: +> Foo: Bar +> Baz: Bat +> +> # CloudFormation (used when Stacks are generated or captured) +> Parameters: +> - ParameterKey: Foo +> ParameterValue: Bar +> - ParameterKey: Baz +> ParameterValue: Bat +> +> # CloudGenesis +> Parameters: +> - Key: Foo +> Value: Bar +> - Key: Baz +> Value: Bat +> ``` *{.Capabilities}*\ @@ -165,6 +184,22 @@ And these constituent parts are used as follows: *{.Tags}*\ > Optional. Tags to use when deploying the Stack. +> +> The *Tags* key can be specified in either of 2 forms: +> +> ``` +> # Natural (recommended) +> Tags: +> Foo: Bar +> Baz: Bat +> +> # CloudFormation / CloudGenesis (used when Stacks are generated or captured) +> Parameters: +> - Key: Foo +> Value: Bar +> - Key: Baz +> Value: Bat +> ``` ## Example From e3548d0137729a98a6aad7023b0c6ce28a27bc44 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 13 Dec 2022 20:53:34 -0500 Subject: [PATCH 009/187] Document valid Capabilities values --- doc/stackctl.1.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/stackctl.1.md b/doc/stackctl.1.md index e85a7dd..754f555 100644 --- a/doc/stackctl.1.md +++ b/doc/stackctl.1.md @@ -180,6 +180,12 @@ And these constituent parts are used as follows: *{.Capabilities}*\ > Optional. Capabilities to use when deploying the Stack. +> +> Valid *Capabilities* are, +> +> **CAPABILITY_AUTO_EXPAND**,\ +> **CAPABILITY_IAM**, and\ +> **CAPABILITY_NAMED_IAM** *{.Tags}*\ From 629bd00b0ca36c4f27bb61cd945cfa27f469629d Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 13 Dec 2022 21:00:54 -0500 Subject: [PATCH 010/187] Tweak sub-command descriptions in the man-page --- doc/stackctl.1.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/stackctl.1.md b/doc/stackctl.1.md index 754f555..621b038 100644 --- a/doc/stackctl.1.md +++ b/doc/stackctl.1.md @@ -38,21 +38,21 @@ stackctl - manage CloudFormation Stacks through specifications **capture**\ -> Generate specifications from deployed Stacks. +> Generate specifications for already-deployed Stacks. **changes**\ -> Show changes between specifications and deployed state. +> Show changes between on-disk specifications and their deployed state. **deploy**\ -> Make deployed state match specifications. +> Make deployed state match on-disk specifications. **version**\ > Print the CLI's version. -See individual man-pages for more details. +Run **man stackctl \** for more details. # Stack Specifications From 19a4e75715f264d842ec81fb044286e805633631 Mon Sep 17 00:00:00 2001 From: Pat Brisbin Date: Tue, 13 Dec 2022 21:13:46 -0500 Subject: [PATCH 011/187] Update src/Stackctl/StackSpecYaml.hs --- src/Stackctl/StackSpecYaml.hs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Stackctl/StackSpecYaml.hs b/src/Stackctl/StackSpecYaml.hs index b656330..d6b9f3f 100644 --- a/src/Stackctl/StackSpecYaml.hs +++ b/src/Stackctl/StackSpecYaml.hs @@ -130,9 +130,6 @@ newtype TagsYaml = TagsYaml instance FromJSON TagsYaml where parseJSON = \case Object o -> do - -- NB. There are simpler ways to do this, but making sure we construct - -- things such that we use (.:) to read the value from each key means that - -- error messages will include "Parameters.{k}". See specs for an example. let parseKey k = do t <- newTag (Key.toText k) <$> o .: k From 33f7f4df5a90b8817b3bba5f1c22f8670265ec2f Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 14 Dec 2022 08:23:03 -0500 Subject: [PATCH 012/187] Use more natural syntax when generating Stacks too --- doc/stackctl.1.md | 4 ++-- src/Stackctl/StackSpecYaml.hs | 16 ++++++++++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/doc/stackctl.1.md b/doc/stackctl.1.md index 621b038..3f77a33 100644 --- a/doc/stackctl.1.md +++ b/doc/stackctl.1.md @@ -162,7 +162,7 @@ And these constituent parts are used as follows: > Foo: Bar > Baz: Bat > -> # CloudFormation (used when Stacks are generated or captured) +> # CloudFormation > Parameters: > - ParameterKey: Foo > ParameterValue: Bar @@ -199,7 +199,7 @@ And these constituent parts are used as follows: > Foo: Bar > Baz: Bat > -> # CloudFormation / CloudGenesis (used when Stacks are generated or captured) +> # CloudFormation / CloudGenesis > Parameters: > - Key: Foo > Value: Bar diff --git a/src/Stackctl/StackSpecYaml.hs b/src/Stackctl/StackSpecYaml.hs index d6b9f3f..aa117bc 100644 --- a/src/Stackctl/StackSpecYaml.hs +++ b/src/Stackctl/StackSpecYaml.hs @@ -72,7 +72,7 @@ instance FromJSON ParametersYaml where -- NB. There are simpler ways to do this, but making sure we construct -- things such that we use (.:) to read the value from each key means that -- error messages will include "Parameters.{k}". See specs for an example. - let parseKey k = ParameterYaml (Key.toText k) <$> o .: k + let parseKey k = ParameterYaml k <$> o .: k ParametersYaml <$> traverse parseKey (KeyMap.keys o) v@Array{} -> ParametersYaml <$> parseJSON v v -> typeMismatch err v @@ -86,17 +86,22 @@ parametersYaml :: [ParameterYaml] -> ParametersYaml parametersYaml = ParametersYaml data ParameterYaml = ParameterYaml - { _pyKey :: Text + { _pyKey :: Key , _pyValue :: Maybe ParameterValue } parameterYaml :: Parameter -> Maybe ParameterYaml parameterYaml p = do k <- p ^. parameter_parameterKey - pure $ ParameterYaml k $ ParameterValue <$> p ^. parameter_parameterKey + pure + $ ParameterYaml (Key.fromText k) + $ ParameterValue + <$> p + ^. parameter_parameterKey unParameterYaml :: ParameterYaml -> Parameter -unParameterYaml (ParameterYaml k v) = makeParameter k $ unParameterValue <$> v +unParameterYaml (ParameterYaml k v) = + makeParameter (Key.toText k) $ unParameterValue <$> v instance FromJSON ParameterYaml where parseJSON = withObject "Parameter" $ \o -> @@ -119,8 +124,7 @@ instance ToJSON ParameterYaml where toEncoding = pairs . mconcat . parameterPairs parameterPairs :: KeyValue a => ParameterYaml -> [a] -parameterPairs (ParameterYaml k v) = - ["ParameterKey" .= k, "ParameterValue" .= v] +parameterPairs (ParameterYaml k v) = [k .= v] newtype TagsYaml = TagsYaml { unTagsYaml :: [TagYaml] From 2ff209c8951fdc8c3a5236c0d68e8fac2bef15dc Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 14 Dec 2022 08:49:30 -0500 Subject: [PATCH 013/187] Use natural syntax in stackctl-cat And add missing Description. --- src/Stackctl/Spec/Cat.hs | 49 ++++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/src/Stackctl/Spec/Cat.hs b/src/Stackctl/Spec/Cat.hs index d81ec06..8468ae9 100644 --- a/src/Stackctl/Spec/Cat.hs +++ b/src/Stackctl/Spec/Cat.hs @@ -122,35 +122,44 @@ 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)] - , ppList - "Parameters" - (ppParameters . map unParameterYaml . unParametersYaml) - ssyParameters + , ppObject "Parameters" parametersYamlKVs ssyParameters , ppList "Capabilities" ppCapabilities ssyCapabilities - , ppList "Tags" (ppTags . map unTagYaml . unTagsYaml) ssyTags + , 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) - ppParameters = concatMap $ \p -> - [ " - " <> cyan "ParameterKey" <> ": " <> maybe - "" - green - (p ^. parameter_parameterKey) - , " " <> cyan "ParameterValue" <> ": " <> maybe - "" - toText - (p ^. parameter_parameterValue) - ] - + ppDescription d = + [cyan "Description" <> ": " <> green (unStackDescription d)] ppCapabilities = map ((" - " <>) . green . toText) - ppTags = concatMap $ \tg -> - [ " - " <> cyan "Key" <> ": " <> green (tg ^. tag_key) - , " " <> cyan "Value" <> ": " <> (tg ^. tag_value) - ] +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 From a0498ac4aebac0fe2b2467675502ccf28d92ceef Mon Sep 17 00:00:00 2001 From: Pat Brisbin Date: Thu, 15 Dec 2022 15:33:53 -0500 Subject: [PATCH 014/187] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 88817d2..a14ebd6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # Stackctl +[![Hackage](https://img.shields.io/hackage/v/stackctl.svg?style=flat)](https://hackage.haskell.org/package/stackctl) +[![CI](https://github.com/freckle/stackctl/actions/workflows/ci.yml/badge.svg)](https://github.com/freckle/stackctl/actions/workflows/ci.yml) + Manage CloudFormation Stacks through specifications. ## About From 4031a9aaa45812116b81f7173c7dd13307473cd7 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 14 Dec 2022 06:04:53 -0500 Subject: [PATCH 015/187] Move filtering later, operate on StackSpec This will allow us to treat filter arguments in other ways, such as matching Stack names or template paths too. Matching names is more intuitive for users, and matching template paths means we can remove the logic of finding changed stacks when template paths change on CI; we can simply pass the template paths to `--filter` and it'll do the Right Thing. --- src/Stackctl/FilterOption.hs | 11 ++++-- src/Stackctl/Spec/Discover.hs | 21 +++++----- src/Stackctl/StackSpec.hs | 15 ++++++- test/Stackctl/FilterOptionSpec.hs | 65 ++++++++++++++++++++++--------- 4 files changed, 81 insertions(+), 31 deletions(-) diff --git a/src/Stackctl/FilterOption.hs b/src/Stackctl/FilterOption.hs index 860215a..53aaf48 100644 --- a/src/Stackctl/FilterOption.hs +++ b/src/Stackctl/FilterOption.hs @@ -3,7 +3,7 @@ module Stackctl.FilterOption , HasFilterOption(..) , filterOption , filterOptionFromPaths - , filterFilePaths + , filterStackSpecs ) where import Stackctl.Prelude @@ -11,6 +11,7 @@ import Stackctl.Prelude import qualified Data.List.NonEmpty as NE import qualified Data.Text as T import Options.Applicative +import Stackctl.StackSpec import System.FilePath.Glob newtype FilterOption = FilterOption @@ -61,5 +62,9 @@ showFilterOption = defaultFilterOption :: FilterOption defaultFilterOption = filterOptionFromPaths $ pure "**/*" -filterFilePaths :: FilterOption -> [FilePath] -> [FilePath] -filterFilePaths fo = filter $ \path -> any (`match` path) $ unFilterOption fo +filterStackSpecs :: FilterOption -> [StackSpec] -> [StackSpec] +filterStackSpecs fo = + filter $ \spec -> any (`matchStackSpec` spec) $ unFilterOption fo + +matchStackSpec :: Pattern -> StackSpec -> Bool +matchStackSpec p = match p . stackSpecStackFile diff --git a/src/Stackctl/Spec/Discover.hs b/src/Stackctl/Spec/Discover.hs index ef285cc..53f7772 100644 --- a/src/Stackctl/Spec/Discover.hs +++ b/src/Stackctl/Spec/Discover.hs @@ -10,7 +10,7 @@ import qualified Data.List.NonEmpty as NE import Stackctl.AWS import Stackctl.AWS.Scope import Stackctl.DirectoryOption (HasDirectoryOption(..)) -import Stackctl.FilterOption (HasFilterOption(..), filterFilePaths) +import Stackctl.FilterOption (HasFilterOption(..), filterStackSpecs) import Stackctl.StackSpec import Stackctl.StackSpecPath import System.FilePath (isPathSeparator) @@ -29,7 +29,7 @@ discoverSpecs discoverSpecs = do dir <- view directoryOptionL scope@AwsScope {..} <- view awsScopeL - discovered <- globRelativeTo + paths <- globRelativeTo dir [ compile $ "stacks" @@ -52,24 +52,27 @@ discoverSpecs = do filterOption <- view filterOptionL let - matched = filterFilePaths filterOption discovered toSpecPath = stackSpecPathFromFilePath scope - (errs, specPaths) = partitionEithers $ map toSpecPath matched + (errs, specPaths) = partitionEithers $ map toSpecPath paths context = [ "path" .= dir , "filters" .= filterOption - , "discovered" .= length discovered - , "matched" .= length matched + , "paths" .= length paths , "errors" .= length errs + , "specs" .= length specPaths ] withThreadContext context $ do - logDebug "Discovered specs" - when (null matched) $ logWarn "No specs found" checkForDuplicateStackNames specPaths - sortStackSpecs <$> traverse (readStackSpec dir) specPaths + specs <- + sortStackSpecs + . filterStackSpecs filterOption + <$> traverse (readStackSpec dir) specPaths + + when (null specs) $ logWarn "No specs found" + specs <$ logDebug ("Discovered specs" :# ["matched" .= length specs]) checkForDuplicateStackNames :: (MonadIO m, MonadLogger m) => [StackSpecPath] -> m () diff --git a/src/Stackctl/StackSpec.hs b/src/Stackctl/StackSpec.hs index fe7a0a5..74c8bc5 100644 --- a/src/Stackctl/StackSpec.hs +++ b/src/Stackctl/StackSpec.hs @@ -7,6 +7,8 @@ module Stackctl.StackSpec , stackSpecActions , stackSpecParameters , stackSpecCapabilities + , stackSpecStackFile + , stackSpecTemplateFile , stackSpecTags , buildStackSpec , TemplateBody @@ -29,6 +31,7 @@ import Stackctl.AWS import Stackctl.Sort import Stackctl.StackSpecPath import Stackctl.StackSpecYaml +import qualified System.FilePath as FilePath import System.FilePath (takeExtension) import UnliftIO.Directory (createDirectoryIfMissing) @@ -56,9 +59,19 @@ stackSpecDepends = fromMaybe [] . ssyDepends . ssSpecBody stackSpecActions :: StackSpec -> [Action] stackSpecActions = fromMaybe [] . ssyActions . ssSpecBody +-- | Normalized, relative path to the @[{root}/]stacks/@ file +stackSpecStackFile :: StackSpec -> FilePath +stackSpecStackFile StackSpec {..} = + FilePath.normalise $ ssSpecRoot stackSpecPathFilePath ssSpecPath + +-- | Normalized, relative path to the @[{root}/]templates/@ file stackSpecTemplateFile :: StackSpec -> StackTemplate stackSpecTemplateFile StackSpec {..} = - StackTemplate $ ssSpecRoot "templates" ssyTemplate ssSpecBody + StackTemplate + $ FilePath.normalise + $ ssSpecRoot + "templates" + ssyTemplate ssSpecBody stackSpecParameters :: StackSpec -> [Parameter] stackSpecParameters = diff --git a/test/Stackctl/FilterOptionSpec.hs b/test/Stackctl/FilterOptionSpec.hs index 66aa543..761b6c6 100644 --- a/test/Stackctl/FilterOptionSpec.hs +++ b/test/Stackctl/FilterOptionSpec.hs @@ -4,32 +4,61 @@ module Stackctl.FilterOptionSpec import Stackctl.Prelude +import Stackctl.AWS +import Stackctl.AWS.Scope import Stackctl.FilterOption +import Stackctl.StackSpec +import Stackctl.StackSpecPath +import Stackctl.StackSpecYaml import Test.Hspec spec :: Spec spec = do - describe "filterFilePaths" $ do - it "filters paths matching any of the given patterns" $ do + describe "filterStackSpecs" $ do + it "filters specs matching any of the given patterns" $ do let option = - filterOptionFromPaths $ "some-path" :| ["prefix/*", "**/suffix"] - paths = - [ "some-path" - , "some-path-other" - , "other-some-path" - , "prefix/foo" - , "prefix/foo-bar" - , "prefix/foo-bar/prefix" - , "foo/suffix" - , "foo/bar/suffix" - , "foo/suffix/bar" + filterOptionFromPaths $ "**/some-path" :| ["**/prefix/*", "**/suffix"] + specs = + [ toSpec "some-path" "some-path" + , toSpec "some-other-path" "some-path-other" + , toSpec "other-some-path" "other-some-path" + , toSpec "prefix-foo" "prefix/foo" + , toSpec "prefix-foo-bar" "prefix/foo-bar" + , toSpec "prefix-foo-bar-prefix" "prefix/foo-bar/prefix" + , toSpec "foo-suffix" "foo/suffix" + , toSpec "foo-bar-suffix" "foo/bar/suffix" + , toSpec "foo-suffix-bar" "foo/suffix/bar" ] - filterFilePaths option paths + map specName (filterStackSpecs option specs) `shouldMatchList` [ "some-path" - , "prefix/foo" - , "prefix/foo-bar" - , "foo/suffix" - , "foo/bar/suffix" + , "prefix-foo" + , "prefix-foo-bar" + , "foo-suffix" + , "foo-bar-suffix" ] + +toSpec :: Text -> FilePath -> StackSpec +toSpec name path = buildStackSpec "." specPath specBody + where + stackName = StackName name + specPath = stackSpecPath scope stackName path + specBody = StackSpecYaml + { ssyDescription = Nothing + , ssyDepends = Nothing + , ssyActions = Nothing + , ssyTemplate = "" + , ssyParameters = Nothing + , ssyCapabilities = Nothing + , ssyTags = Nothing + } + + scope = AwsScope + { awsAccountId = AccountId "1234567890" + , awsAccountName = "test-account" + , awsRegion = Region' "us-east-1" + } + +specName :: StackSpec -> Text +specName = unStackName . stackSpecStackName From 49c2f914b8eb5ee16d45bae1e0db68ff26cb8a52 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 14 Dec 2022 06:14:41 -0500 Subject: [PATCH 016/187] Match spec templates with --filter On CI, we currently get the list of changed stacks or templates, then we work out which stacks use the changed templates, in order to pass their paths to `--filter`. With this support, we can just pass the stacks and templates. --- src/Stackctl/FilterOption.hs | 3 ++- src/Stackctl/StackSpec.hs | 12 ++++------ test/Stackctl/FilterOptionSpec.hs | 38 +++++++++++++++++++++---------- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/src/Stackctl/FilterOption.hs b/src/Stackctl/FilterOption.hs index 53aaf48..e99d247 100644 --- a/src/Stackctl/FilterOption.hs +++ b/src/Stackctl/FilterOption.hs @@ -67,4 +67,5 @@ filterStackSpecs fo = filter $ \spec -> any (`matchStackSpec` spec) $ unFilterOption fo matchStackSpec :: Pattern -> StackSpec -> Bool -matchStackSpec p = match p . stackSpecStackFile +matchStackSpec p spec = + or [match p $ stackSpecStackFile spec, match p $ stackSpecTemplateFile spec] diff --git a/src/Stackctl/StackSpec.hs b/src/Stackctl/StackSpec.hs index 74c8bc5..76b004f 100644 --- a/src/Stackctl/StackSpec.hs +++ b/src/Stackctl/StackSpec.hs @@ -65,13 +65,9 @@ stackSpecStackFile StackSpec {..} = FilePath.normalise $ ssSpecRoot stackSpecPathFilePath ssSpecPath -- | Normalized, relative path to the @[{root}/]templates/@ file -stackSpecTemplateFile :: StackSpec -> StackTemplate +stackSpecTemplateFile :: StackSpec -> FilePath stackSpecTemplateFile StackSpec {..} = - StackTemplate - $ FilePath.normalise - $ ssSpecRoot - "templates" - ssyTemplate ssSpecBody + FilePath.normalise $ ssSpecRoot "templates" ssyTemplate ssSpecBody stackSpecParameters :: StackSpec -> [Parameter] stackSpecParameters = @@ -130,7 +126,7 @@ writeStackSpec parent stackSpec@StackSpec {..} templateBody = do createDirectoryIfMissing True $ takeDirectory specPath liftIO $ Yaml.encodeFile specPath ssSpecBody where - templatePath = unStackTemplate $ stackSpecTemplateFile stackSpec + templatePath = stackSpecTemplateFile stackSpec specPath = parent stackSpecPathFilePath ssSpecPath readStackSpec :: MonadIO m => FilePath -> StackSpecPath -> m StackSpec @@ -161,7 +157,7 @@ createChangeSet createChangeSet spec parameters = awsCloudFormationCreateChangeSet (stackSpecStackName spec) (stackSpecStackDescription spec) - (stackSpecTemplateFile spec) + (StackTemplate $ stackSpecTemplateFile spec) (nubOrdOn (^. parameter_parameterKey) $ parameters <> stackSpecParameters spec ) (stackSpecCapabilities spec) diff --git a/test/Stackctl/FilterOptionSpec.hs b/test/Stackctl/FilterOptionSpec.hs index 761b6c6..7b1dc83 100644 --- a/test/Stackctl/FilterOptionSpec.hs +++ b/test/Stackctl/FilterOptionSpec.hs @@ -20,15 +20,15 @@ spec = do option = filterOptionFromPaths $ "**/some-path" :| ["**/prefix/*", "**/suffix"] specs = - [ toSpec "some-path" "some-path" - , toSpec "some-other-path" "some-path-other" - , toSpec "other-some-path" "other-some-path" - , toSpec "prefix-foo" "prefix/foo" - , toSpec "prefix-foo-bar" "prefix/foo-bar" - , toSpec "prefix-foo-bar-prefix" "prefix/foo-bar/prefix" - , toSpec "foo-suffix" "foo/suffix" - , toSpec "foo-bar-suffix" "foo/bar/suffix" - , toSpec "foo-suffix-bar" "foo/suffix/bar" + [ toSpec "some-path" "some-path" Nothing + , toSpec "some-other-path" "some-path-other" Nothing + , toSpec "other-some-path" "other-some-path" Nothing + , toSpec "prefix-foo" "prefix/foo" Nothing + , toSpec "prefix-foo-bar" "prefix/foo-bar" Nothing + , toSpec "prefix-foo-bar-prefix" "prefix/foo-bar/prefix" Nothing + , toSpec "foo-suffix" "foo/suffix" Nothing + , toSpec "foo-bar-suffix" "foo/bar/suffix" Nothing + , toSpec "foo-suffix-bar" "foo/suffix/bar" Nothing ] map specName (filterStackSpecs option specs) @@ -39,8 +39,22 @@ spec = do , "foo-bar-suffix" ] -toSpec :: Text -> FilePath -> StackSpec -toSpec name path = buildStackSpec "." specPath specBody + it "filters specs by template too" $ do + let + option = filterOptionFromPaths $ "templates/x" :| ["**/y/*"] + specs = + [ toSpec "some-path" "some-path" Nothing + , toSpec "some-other-path" "some-path-other" $ Just "x" + , toSpec "prefix-foo" "prefix/foo" Nothing + , toSpec "other-some-path" "other-some-path" $ Just "z/y/t" + , toSpec "prefix-foo-bar" "prefix/foo-bar" Nothing + ] + + map specName (filterStackSpecs option specs) + `shouldMatchList` ["some-other-path", "other-some-path"] + +toSpec :: Text -> FilePath -> Maybe FilePath -> StackSpec +toSpec name path mTemplate = buildStackSpec "." specPath specBody where stackName = StackName name specPath = stackSpecPath scope stackName path @@ -48,7 +62,7 @@ toSpec name path = buildStackSpec "." specPath specBody { ssyDescription = Nothing , ssyDepends = Nothing , ssyActions = Nothing - , ssyTemplate = "" + , ssyTemplate = fromMaybe path mTemplate , ssyParameters = Nothing , ssyCapabilities = Nothing , ssyTags = Nothing From 930c6793291b20756a4864dce65c0cd84b939b11 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 14 Dec 2022 07:31:00 -0500 Subject: [PATCH 017/187] Match stack names by --filter too This is more intuitive for end-users. --- src/Stackctl/FilterOption.hs | 8 ++++++-- test/Stackctl/FilterOptionSpec.hs | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/Stackctl/FilterOption.hs b/src/Stackctl/FilterOption.hs index e99d247..9a7076e 100644 --- a/src/Stackctl/FilterOption.hs +++ b/src/Stackctl/FilterOption.hs @@ -11,6 +11,7 @@ import Stackctl.Prelude import qualified Data.List.NonEmpty as NE import qualified Data.Text as T import Options.Applicative +import Stackctl.AWS.CloudFormation (StackName(..)) import Stackctl.StackSpec import System.FilePath.Glob @@ -67,5 +68,8 @@ filterStackSpecs fo = filter $ \spec -> any (`matchStackSpec` spec) $ unFilterOption fo matchStackSpec :: Pattern -> StackSpec -> Bool -matchStackSpec p spec = - or [match p $ stackSpecStackFile spec, match p $ stackSpecTemplateFile spec] +matchStackSpec p spec = or + [ match p $ unpack $ unStackName $ stackSpecStackName spec + , match p $ stackSpecStackFile spec + , match p $ stackSpecTemplateFile spec + ] diff --git a/test/Stackctl/FilterOptionSpec.hs b/test/Stackctl/FilterOptionSpec.hs index 7b1dc83..e940c05 100644 --- a/test/Stackctl/FilterOptionSpec.hs +++ b/test/Stackctl/FilterOptionSpec.hs @@ -53,6 +53,25 @@ spec = do map specName (filterStackSpecs option specs) `shouldMatchList` ["some-other-path", "other-some-path"] + it "filters specs by name too" $ do + let + option = + filterOptionFromPaths $ "some-name" :| ["**/prefix/*", "templates/x"] + specs = + [ toSpec "some-name" "some-path" Nothing + , toSpec "some-path" "some-path-other" $ Just "x" + , toSpec "prefix-foo" "prefix/foo" Nothing + , toSpec "other-some-path" "other-some-path" $ Just "z/y/t" + , toSpec "prefix-foo-bar" "prefix/foo-bar" Nothing + ] + + map specName (filterStackSpecs option specs) + `shouldMatchList` [ "some-name" + , "some-path" + , "prefix-foo" + , "prefix-foo-bar" + ] + toSpec :: Text -> FilePath -> Maybe FilePath -> StackSpec toSpec name path mTemplate = buildStackSpec "." specPath specBody where From 3aa1af0cb30440bc80bf2468bca269ac152d6f72 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 14 Dec 2022 07:31:36 -0500 Subject: [PATCH 018/187] Match --filter patterns more loosely Automatically add prefixes and suffixes to support filters matching portions of paths more intuitively. Examples: ``` --filter some/thing => [ some/thing -- as-is , **/some/thing -- at any depth , **/some/thing/* -- as a directory , **/some/thing.json -- with expected extensions , **/some/thing.yaml , **/some/thing.yml ] --filter some/thing.ext => [ some/thing.ext -- as-is , **/some/thing.ext -- at any depth ] ``` --- src/Stackctl/FilterOption.hs | 29 ++++++++++++++++++++++++----- test/Stackctl/FilterOptionSpec.hs | 25 ++++++++++++++++++++----- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/src/Stackctl/FilterOption.hs b/src/Stackctl/FilterOption.hs index 9a7076e..2e96565 100644 --- a/src/Stackctl/FilterOption.hs +++ b/src/Stackctl/FilterOption.hs @@ -3,6 +3,7 @@ module Stackctl.FilterOption , HasFilterOption(..) , filterOption , filterOptionFromPaths + , filterOptionFromText , filterStackSpecs ) where @@ -13,6 +14,7 @@ import qualified Data.Text as T import Options.Applicative import Stackctl.AWS.CloudFormation (StackName(..)) import Stackctl.StackSpec +import System.FilePath (hasExtension) import System.FilePath.Glob newtype FilterOption = FilterOption @@ -41,15 +43,32 @@ filterOption items = option (eitherReader readFilterOption) $ mconcat filterOptionFromPaths :: NonEmpty FilePath -> FilterOption filterOptionFromPaths = FilterOption . fmap compile -readFilterOption :: String -> Either String FilterOption -readFilterOption = - maybe (Left err) (Right . FilterOption) +filterOptionFromText :: Text -> Maybe FilterOption +filterOptionFromText = + fmap FilterOption . NE.nonEmpty - . map (compile . unpack) + . concatMap expandPatterns . filter (not . T.null) . map T.strip . T.splitOn "," - . pack + +expandPatterns :: Text -> [Pattern] +expandPatterns t = map compile $ s : expanded + where + expanded + | "**" `T.isPrefixOf` t = suffixed + | otherwise = map ("**" ) $ s : suffixed + + suffixed + | "*" `T.isSuffixOf` t || hasExtension s = [] + | otherwise = (s "*") : map (s <.>) extensions + + extensions = ["json", "yaml"] + + s = unpack t + +readFilterOption :: String -> Either String FilterOption +readFilterOption = note err . filterOptionFromText . pack where err = "Must be non-empty, comma-separated list of non-empty patterns" showFilterOption :: FilterOption -> String diff --git a/test/Stackctl/FilterOptionSpec.hs b/test/Stackctl/FilterOptionSpec.hs index e940c05..30402b8 100644 --- a/test/Stackctl/FilterOptionSpec.hs +++ b/test/Stackctl/FilterOptionSpec.hs @@ -1,3 +1,5 @@ +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} + module Stackctl.FilterOptionSpec ( spec ) where @@ -17,8 +19,7 @@ spec = do describe "filterStackSpecs" $ do it "filters specs matching any of the given patterns" $ do let - option = - filterOptionFromPaths $ "**/some-path" :| ["**/prefix/*", "**/suffix"] + Just option = filterOptionFromText "**/some-path,**/prefix/*,**/suffix" specs = [ toSpec "some-path" "some-path" Nothing , toSpec "some-other-path" "some-path-other" Nothing @@ -37,11 +38,12 @@ spec = do , "prefix-foo-bar" , "foo-suffix" , "foo-bar-suffix" + , "foo-suffix-bar" ] it "filters specs by template too" $ do let - option = filterOptionFromPaths $ "templates/x" :| ["**/y/*"] + Just option = filterOptionFromText "templates/x,**/y/*" specs = [ toSpec "some-path" "some-path" Nothing , toSpec "some-other-path" "some-path-other" $ Just "x" @@ -55,8 +57,7 @@ spec = do it "filters specs by name too" $ do let - option = - filterOptionFromPaths $ "some-name" :| ["**/prefix/*", "templates/x"] + Just option = filterOptionFromText "some-name,**/prefix/*,templates/x" specs = [ toSpec "some-name" "some-path" Nothing , toSpec "some-path" "some-path-other" $ Just "x" @@ -72,6 +73,20 @@ spec = do , "prefix-foo-bar" ] + it "adds some intuitive fuzziness" $ do + let + Just option = filterOptionFromText "some/path,file,file.ext" + specs = + [ toSpec "some-name" "x/some/path/y" Nothing + , toSpec "some-path" "some-path/other" $ Just "x" + , toSpec "prefix-foo" "prefix/file.json" Nothing + , toSpec "other-some-path" "other-some-path" $ Just "z/y/t" + , toSpec "prefix-foo-bar" "prefix/foo-bar" Nothing + ] + + map specName (filterStackSpecs option specs) + `shouldMatchList` ["some-name", "prefix-foo"] + toSpec :: Text -> FilePath -> Maybe FilePath -> StackSpec toSpec name path mTemplate = buildStackSpec "." specPath specBody where From 9ec0244c76d476df4f10567bb751655af37807c8 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 14 Dec 2022 08:04:40 -0500 Subject: [PATCH 019/187] Don't duplicate templates in stackctl-cat If you list Stacks that share a template, they were being repeated. --- src/Stackctl/Spec/Cat.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Stackctl/Spec/Cat.hs b/src/Stackctl/Spec/Cat.hs index 8468ae9..f4a340e 100644 --- a/src/Stackctl/Spec/Cat.hs +++ b/src/Stackctl/Spec/Cat.hs @@ -100,7 +100,7 @@ runCat CatOptions {..} = do pure $ ssyTemplate body putTemplate 2 "templates/" - for_ (sort $ concat $ concat templates) $ \template -> do + for_ (sort $ nubOrd $ concat $ concat templates) $ \template -> do val <- Yaml.decodeFileThrow @_ @Value $ dir "templates" template putTemplate 4 $ green $ fromString template From 8c7d525b76fa9d927282ad380e34484e82113cd4 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Thu, 15 Dec 2022 12:25:41 -0500 Subject: [PATCH 020/187] Fix bug in parameterYaml This function used the key twice to build the Parameter; oops. This doesn't affect most CLI usage since we don't construct values through this function when reading Yaml. It does affect `capture` and library-usage of `Generate`, where we do. --- src/Stackctl/StackSpecYaml.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Stackctl/StackSpecYaml.hs b/src/Stackctl/StackSpecYaml.hs index aa117bc..b8818b0 100644 --- a/src/Stackctl/StackSpecYaml.hs +++ b/src/Stackctl/StackSpecYaml.hs @@ -97,7 +97,7 @@ parameterYaml p = do $ ParameterYaml (Key.fromText k) $ ParameterValue <$> p - ^. parameter_parameterKey + ^. parameter_parameterValue unParameterYaml :: ParameterYaml -> Parameter unParameterYaml (ParameterYaml k v) = From 564678203fe70b5c4c46c655dd3daeaafb6de9e0 Mon Sep 17 00:00:00 2001 From: Pat Brisbin Date: Mon, 19 Dec 2022 11:32:00 -0500 Subject: [PATCH 021/187] Repository-local configuration If present, `./.stackctl/config.yaml` is read on startup and loaded into an application `Config` value. This configuration provides two abilities: - To specify a version requirement, in case your specs are relying on certain Stackctl features and/or bugfixes and you'd like to fully ensure behaviors in both local and CI contexts - To specify some `defaults`: `Parameters` or `Tags` that should be applied to all Stacks deployed from this location. For example, `App`, `Owner`, or `DeployedBy`. It's tedious and error-prone to have to specify repeated things in every specification. The config currently look like this (all values optional): ```yaml required_version: defaults: parameters: tags: ``` And here is an example: ```yaml required_version: =~ 1.2 defaults: parameters: App: my-cool-app tags: Owner: my-cool-team ``` To support this, - `RequiredVersion` was built and tested - `ParametersYaml` and `TagsYaml` were given "last-wins" `Semigroup` instances - `Config` and `HasConfig` were built - `StackSpec` construction was centralized in `buildStackSpec`, which grew a `HasConfig` constraint, which it now uses to apply `defaults` for every `StackSpec` we ever construct --- package.yaml | 4 + src/Stackctl/CLI.hs | 19 ++-- src/Stackctl/Commands.hs | 6 +- src/Stackctl/Config.hs | 106 ++++++++++++++++++++ src/Stackctl/Config/RequiredVersion.hs | 81 +++++++++++++++ src/Stackctl/Spec/Capture.hs | 2 + src/Stackctl/Spec/Cat.hs | 2 + src/Stackctl/Spec/Changes.hs | 2 + src/Stackctl/Spec/Deploy.hs | 4 +- src/Stackctl/Spec/Discover.hs | 2 + src/Stackctl/Spec/Generate.hs | 4 +- src/Stackctl/StackSpec.hs | 29 ++++-- src/Stackctl/StackSpecYaml.hs | 52 ++++++++-- stackctl.cabal | 10 +- test/Stackctl/Config/RequiredVersionSpec.hs | 80 +++++++++++++++ test/Stackctl/ConfigSpec.hs | 75 ++++++++++++++ test/Stackctl/FilterOptionSpec.hs | 4 +- test/Stackctl/StackSpecSpec.hs | 4 +- test/Stackctl/StackSpecYamlSpec.hs | 26 +++++ 19 files changed, 479 insertions(+), 33 deletions(-) create mode 100644 src/Stackctl/Config.hs create mode 100644 src/Stackctl/Config/RequiredVersion.hs create mode 100644 test/Stackctl/Config/RequiredVersionSpec.hs create mode 100644 test/Stackctl/ConfigSpec.hs diff --git a/package.yaml b/package.yaml index 1d9c5b7..a4a8d23 100644 --- a/package.yaml +++ b/package.yaml @@ -79,6 +79,7 @@ library: - lens - lens-aeson - monad-logger + - mtl - optparse-applicative - resourcet - rio @@ -106,6 +107,9 @@ tests: main: Spec.hs source-dirs: test dependencies: + - QuickCheck + - bytestring - hspec + - mtl - stackctl - yaml diff --git a/src/Stackctl/CLI.hs b/src/Stackctl/CLI.hs index dad459c..32d7d6f 100644 --- a/src/Stackctl/CLI.hs +++ b/src/Stackctl/CLI.hs @@ -13,12 +13,14 @@ import Control.Monad.Trans.Resource (ResourceT, runResourceT) import Stackctl.AWS import Stackctl.AWS.Scope import Stackctl.ColorOption +import Stackctl.Config import Stackctl.DirectoryOption import Stackctl.FilterOption import Stackctl.VerboseOption data App options = App { appLogger :: Logger + , appConfig :: Config , appOptions :: options , appAwsScope :: AwsScope , appAwsEnv :: AwsEnv @@ -30,6 +32,9 @@ optionsL = lens appOptions $ \x y -> x { appOptions = y } instance HasLogger (App options) where loggerL = lens appLogger $ \x y -> x { appLogger = y } +instance HasConfig (App options) where + configL = lens appConfig $ \x y -> x { appConfig = y } + instance HasAwsScope (App options) where awsScopeL = lens appAwsScope $ \x y -> x { appAwsScope = y } @@ -86,14 +91,14 @@ runAppT options f = do (options ^. verboseOptionL) envLogSettings - aws <- runLoggerLoggingT logger awsEnvDiscover - - let - runAws - :: MonadUnliftIO m => ReaderT AwsEnv (LoggingT (ResourceT m)) a -> m a - runAws = runResourceT . runLoggerLoggingT logger . flip runReaderT aws + app <- runResourceT $ runLoggerLoggingT logger $ do + aws <- awsEnvDiscover - app <- App logger options <$> runAws fetchAwsScope <*> pure aws + App logger + <$> loadConfigOrExit + <*> pure options + <*> runReaderT fetchAwsScope aws + <*> pure aws let AwsScope {..} = appAwsScope app diff --git a/src/Stackctl/Commands.hs b/src/Stackctl/Commands.hs index c198fb2..32a9fe1 100644 --- a/src/Stackctl/Commands.hs +++ b/src/Stackctl/Commands.hs @@ -11,6 +11,7 @@ import Stackctl.Prelude import Stackctl.AWS import Stackctl.AWS.Scope import Stackctl.Colors +import Stackctl.Config (HasConfig) import Stackctl.DirectoryOption import Stackctl.FilterOption import Stackctl.Spec.Capture @@ -23,6 +24,7 @@ import Stackctl.Version cat :: ( HasLogger env , HasAwsScope env + , HasConfig env , HasDirectoryOption env , HasFilterOption env , HasColorOption env @@ -36,7 +38,7 @@ cat = Subcommand } capture - :: (HasAwsScope env, HasAwsEnv env, HasDirectoryOption env) + :: (HasAwsScope env, HasAwsEnv env, HasConfig env, HasDirectoryOption env) => Subcommand CaptureOptions env capture = Subcommand { name = "capture" @@ -49,6 +51,7 @@ changes :: ( HasLogger env , HasAwsScope env , HasAwsEnv env + , HasConfig env , HasDirectoryOption env , HasFilterOption env ) @@ -64,6 +67,7 @@ deploy :: ( HasLogger env , HasAwsScope env , HasAwsEnv env + , HasConfig env , HasDirectoryOption env , HasFilterOption env ) diff --git a/src/Stackctl/Config.hs b/src/Stackctl/Config.hs new file mode 100644 index 0000000..aedac47 --- /dev/null +++ b/src/Stackctl/Config.hs @@ -0,0 +1,106 @@ +module Stackctl.Config + ( Config(..) + , configParameters + , configTags + , emptyConfig + , HasConfig(..) + , ConfigError(..) + , loadConfigOrExit + , loadConfigFromBytes + , applyConfig + ) where + +import Stackctl.Prelude + +import Control.Monad.Except +import Data.Aeson +import Data.Version +import qualified Data.Yaml as Yaml +import Paths_stackctl as Paths +import Stackctl.Config.RequiredVersion +import Stackctl.StackSpecYaml +import UnliftIO.Directory (doesFileExist) + +data Config = Config + { required_version :: Maybe RequiredVersion + , defaults :: Maybe Defaults + } + deriving stock Generic + deriving anyclass FromJSON + +configParameters :: Config -> Maybe ParametersYaml +configParameters = parameters <=< defaults + +configTags :: Config -> Maybe TagsYaml +configTags = tags <=< defaults + +emptyConfig :: Config +emptyConfig = Config Nothing Nothing + +data Defaults = Defaults + { parameters :: Maybe ParametersYaml + , tags :: Maybe TagsYaml + } + deriving stock Generic + deriving anyclass FromJSON + +class HasConfig env where + configL :: Lens' env Config + +instance HasConfig Config where + configL = id + +data ConfigError + = ConfigInvalidYaml Yaml.ParseException + | ConfigInvalid (NonEmpty Text) + | ConfigVersionNotSatisfied RequiredVersion Version + deriving stock Show + +configErrorMessage :: ConfigError -> Message +configErrorMessage = \case + ConfigInvalidYaml ex -> + "Configuration is not valid Yaml" + :# ["error" .= Yaml.prettyPrintParseException ex] + ConfigInvalid errs -> "Invalid configuration" :# ["errors" .= errs] + ConfigVersionNotSatisfied rv v -> + "Incompatible Stackctl version" :# ["current" .= v, "required" .= show rv] + +loadConfigOrExit :: (MonadIO m, MonadLogger m) => m Config +loadConfigOrExit = either die pure =<< loadConfig + where + die e = do + logError $ configErrorMessage e + exitFailure + +loadConfig :: MonadIO m => m (Either ConfigError Config) +loadConfig = runExceptT $ getConfigFile >>= \case + Nothing -> pure emptyConfig + Just cf -> loadConfigFrom cf + +loadConfigFrom :: (MonadIO m, MonadError ConfigError m) => FilePath -> m Config +loadConfigFrom path = loadConfigFromBytes =<< liftIO (readFileBinary path) + +loadConfigFromBytes :: MonadError ConfigError m => ByteString -> m Config +loadConfigFromBytes bs = do + config <- either (throwError . ConfigInvalidYaml) pure $ Yaml.decodeEither' bs + config <$ traverse_ checkRequiredVersion (required_version config) + where + checkRequiredVersion rv = + unless (isRequiredVersionSatisfied rv Paths.version) + $ throwError + $ ConfigVersionNotSatisfied rv Paths.version + +applyConfig :: Config -> StackSpecYaml -> StackSpecYaml +applyConfig config ss@StackSpecYaml {..} = ss + { ssyParameters = configParameters config <> ssyParameters + , ssyTags = configTags config <> ssyTags + } + +getConfigFile :: MonadIO m => m (Maybe FilePath) +getConfigFile = listToMaybe <$> filterM + doesFileExist + [ ".stackctl" "config" <.> "yaml" + , ".stackctl" "config" <.> "yml" + , ".stackctl" <.> "yaml" + , ".stackctl" <.> "yml" + ] diff --git a/src/Stackctl/Config/RequiredVersion.hs b/src/Stackctl/Config/RequiredVersion.hs new file mode 100644 index 0000000..8aae43e --- /dev/null +++ b/src/Stackctl/Config/RequiredVersion.hs @@ -0,0 +1,81 @@ +module Stackctl.Config.RequiredVersion + ( RequiredVersion(..) + , requiredVersionFromText + , isRequiredVersionSatisfied + + -- * Exported for testing + , (=~) + ) where + +import Stackctl.Prelude + +import Data.Aeson +import Data.List (uncons) +import qualified Data.List.NonEmpty as NE +import qualified Data.Text as T +import Data.Version hiding (parseVersion) +import qualified Data.Version as Version +import Text.ParserCombinators.ReadP (readP_to_S) + +data RequiredVersion = RequiredVersion + { requiredVersionOp :: Text + , requiredVersionCompare :: Version -> Version -> Bool + , requiredVersionCompareWith :: Version + } + +instance Show RequiredVersion where + show RequiredVersion {..} = + unpack requiredVersionOp <> " " <> showVersion requiredVersionCompareWith + +instance FromJSON RequiredVersion where + parseJSON = + withText "RequiredVersion" $ either fail pure . requiredVersionFromText + +requiredVersionFromText :: Text -> Either String RequiredVersion +requiredVersionFromText = fromWords . T.words + where + fromWords :: [Text] -> Either String RequiredVersion + fromWords = \case + [w] -> parseRequiredVersion "=" w + [op, w] -> parseRequiredVersion op w + ws -> + Left + $ show (unpack $ T.unwords ws) + <> " did not parse as optional operator and version string" + + parseRequiredVersion :: Text -> Text -> Either String RequiredVersion + parseRequiredVersion op w = do + v <- parseVersion w + + case op of + "=" -> Right $ RequiredVersion op (==) v + "<" -> Right $ RequiredVersion op (<) v + "<=" -> Right $ RequiredVersion op (<=) v + ">" -> Right $ RequiredVersion op (>) v + ">=" -> Right $ RequiredVersion op (>=) v + "=~" -> Right $ RequiredVersion op (=~) v + _ -> + Left + $ "Invalid comparison operator (" + <> unpack op + <> "), may only be =, <, <=, >, >=, or =~" + + parseVersion :: Text -> Either String Version + parseVersion t = + fmap (fst . NE.last) + $ note ("Failed to parse as a version " <> s) + $ NE.nonEmpty + $ readP_to_S Version.parseVersion s + where s = unpack t + +(=~) :: Version -> Version -> Bool +a =~ b = a >= b && a < incrementVersion b + where + incrementVersion = onVersion $ backwards $ onHead (+ 1) + onVersion f = makeVersion . f . versionBranch + backwards f = reverse . f . reverse + onHead f as = maybe as (uncurry (:) . first f) $ uncons as + +isRequiredVersionSatisfied :: RequiredVersion -> Version -> Bool +isRequiredVersionSatisfied RequiredVersion {..} = + (`requiredVersionCompare` requiredVersionCompareWith) diff --git a/src/Stackctl/Spec/Capture.hs b/src/Stackctl/Spec/Capture.hs index 5f2b22a..6e4a8c3 100644 --- a/src/Stackctl/Spec/Capture.hs +++ b/src/Stackctl/Spec/Capture.hs @@ -9,6 +9,7 @@ import Stackctl.Prelude import Options.Applicative import Stackctl.AWS import Stackctl.AWS.Scope +import Stackctl.Config (HasConfig) import Stackctl.DirectoryOption (HasDirectoryOption(..)) import Stackctl.Spec.Generate import Stackctl.StackSpec @@ -66,6 +67,7 @@ runCapture , MonadReader env m , HasAwsScope env , HasAwsEnv env + , HasConfig env , HasDirectoryOption env ) => CaptureOptions diff --git a/src/Stackctl/Spec/Cat.hs b/src/Stackctl/Spec/Cat.hs index f4a340e..4d5d102 100644 --- a/src/Stackctl/Spec/Cat.hs +++ b/src/Stackctl/Spec/Cat.hs @@ -20,6 +20,7 @@ import Options.Applicative import Stackctl.AWS import Stackctl.AWS.Scope import Stackctl.Colors +import Stackctl.Config (HasConfig) import Stackctl.DirectoryOption (HasDirectoryOption(..)) import Stackctl.FilterOption (HasFilterOption) import Stackctl.Spec.Discover @@ -58,6 +59,7 @@ runCat , MonadReader env m , HasLogger env , HasAwsScope env + , HasConfig env , HasDirectoryOption env , HasFilterOption env , HasColorOption env diff --git a/src/Stackctl/Spec/Changes.hs b/src/Stackctl/Spec/Changes.hs index 0aae52a..3e40132 100644 --- a/src/Stackctl/Spec/Changes.hs +++ b/src/Stackctl/Spec/Changes.hs @@ -12,6 +12,7 @@ import Options.Applicative import Stackctl.AWS hiding (action) import Stackctl.AWS.Scope import Stackctl.Colors +import Stackctl.Config (HasConfig) import Stackctl.DirectoryOption (HasDirectoryOption) import Stackctl.FilterOption (HasFilterOption) import Stackctl.ParameterOption @@ -47,6 +48,7 @@ runChanges , HasLogger env , HasAwsScope env , HasAwsEnv env + , HasConfig env , HasDirectoryOption env , HasFilterOption env ) diff --git a/src/Stackctl/Spec/Deploy.hs b/src/Stackctl/Spec/Deploy.hs index 9fed04f..ce07dd4 100644 --- a/src/Stackctl/Spec/Deploy.hs +++ b/src/Stackctl/Spec/Deploy.hs @@ -11,10 +11,11 @@ import Blammo.Logging.Logger (pushLoggerLn) import qualified Data.Text as T import Data.Time (defaultTimeLocale, formatTime, utcToLocalZonedTime) import Options.Applicative +import Stackctl.Action import Stackctl.AWS hiding (action) import Stackctl.AWS.Scope -import Stackctl.Action import Stackctl.Colors +import Stackctl.Config (HasConfig) import Stackctl.DirectoryOption (HasDirectoryOption) import Stackctl.FilterOption (HasFilterOption) import Stackctl.ParameterOption @@ -60,6 +61,7 @@ runDeploy , HasLogger env , HasAwsScope env , HasAwsEnv env + , HasConfig env , HasDirectoryOption env , HasFilterOption env ) diff --git a/src/Stackctl/Spec/Discover.hs b/src/Stackctl/Spec/Discover.hs index 53f7772..a109aa6 100644 --- a/src/Stackctl/Spec/Discover.hs +++ b/src/Stackctl/Spec/Discover.hs @@ -9,6 +9,7 @@ import Data.List.Extra (dropPrefix) import qualified Data.List.NonEmpty as NE import Stackctl.AWS import Stackctl.AWS.Scope +import Stackctl.Config (HasConfig) import Stackctl.DirectoryOption (HasDirectoryOption(..)) import Stackctl.FilterOption (HasFilterOption(..), filterStackSpecs) import Stackctl.StackSpec @@ -22,6 +23,7 @@ discoverSpecs , MonadLogger m , MonadReader env m , HasAwsScope env + , HasConfig env , HasDirectoryOption env , HasFilterOption env ) diff --git a/src/Stackctl/Spec/Generate.hs b/src/Stackctl/Spec/Generate.hs index d3e73fe..4b5ca6f 100644 --- a/src/Stackctl/Spec/Generate.hs +++ b/src/Stackctl/Spec/Generate.hs @@ -9,6 +9,7 @@ import Stackctl.Prelude import Stackctl.Action import Stackctl.AWS import Stackctl.AWS.Scope +import Stackctl.Config (HasConfig) import Stackctl.Spec.Discover (buildSpecPath) import Stackctl.StackSpec import Stackctl.StackSpecPath @@ -41,6 +42,7 @@ generate , MonadUnliftIO m , MonadLogger m , MonadReader env m + , HasConfig env , HasAwsScope env ) => Generate @@ -69,7 +71,7 @@ generate Generate {..} = do , ssyTags = tagsYaml . map TagYaml <$> gTags } - stackSpec = buildStackSpec gOutputDirectory specPath specYaml + stackSpec <- buildStackSpec gOutputDirectory specPath specYaml withThreadContext ["stackName" .= stackSpecStackName stackSpec] $ do logInfo "Generating specification" diff --git a/src/Stackctl/StackSpec.hs b/src/Stackctl/StackSpec.hs index 76b004f..1fa5a79 100644 --- a/src/Stackctl/StackSpec.hs +++ b/src/Stackctl/StackSpec.hs @@ -28,6 +28,7 @@ import Data.List.Extra (nubOrdOn) import qualified Data.Yaml as Yaml import Stackctl.Action import Stackctl.AWS +import Stackctl.Config (HasConfig(..), applyConfig) import Stackctl.Sort import Stackctl.StackSpecPath import Stackctl.StackSpecYaml @@ -79,8 +80,19 @@ stackSpecCapabilities = fromMaybe [] . ssyCapabilities . ssSpecBody stackSpecTags :: StackSpec -> [Tag] stackSpecTags = maybe [] (map unTagYaml . unTagsYaml) . ssyTags . ssSpecBody -buildStackSpec :: FilePath -> StackSpecPath -> StackSpecYaml -> StackSpec -buildStackSpec = StackSpec +buildStackSpec + :: (MonadReader env m, HasConfig env) + => FilePath + -> StackSpecPath + -> StackSpecYaml + -> m StackSpec +buildStackSpec dir specPath specBody = do + config <- view configL + pure StackSpec + { ssSpecRoot = dir + , ssSpecPath = specPath + , ssSpecBody = applyConfig config specBody + } data TemplateBody = TemplateText Text @@ -129,15 +141,14 @@ writeStackSpec parent stackSpec@StackSpec {..} templateBody = do templatePath = stackSpecTemplateFile stackSpec specPath = parent stackSpecPathFilePath ssSpecPath -readStackSpec :: MonadIO m => FilePath -> StackSpecPath -> m StackSpec +readStackSpec + :: (MonadIO m, MonadReader env m, HasConfig env) + => FilePath + -> StackSpecPath + -> m StackSpec readStackSpec dir specPath = do specBody <- liftIO $ either err pure =<< Yaml.decodeFileEither path - - pure StackSpec - { ssSpecRoot = dir - , ssSpecPath = specPath - , ssSpecBody = specBody - } + buildStackSpec dir specPath specBody where path = dir stackSpecPathFilePath specPath err e = diff --git a/src/Stackctl/StackSpecYaml.hs b/src/Stackctl/StackSpecYaml.hs index b8818b0..d858050 100644 --- a/src/Stackctl/StackSpecYaml.hs +++ b/src/Stackctl/StackSpecYaml.hs @@ -39,6 +39,8 @@ import Data.Aeson.Casing import qualified Data.Aeson.Key as Key import qualified Data.Aeson.KeyMap as KeyMap import Data.Aeson.Types (typeMismatch) +import qualified Data.HashMap.Strict as HashMap +import Data.Monoid (Last(..)) import qualified Data.Text as T import Stackctl.Action import Stackctl.AWS @@ -64,8 +66,19 @@ instance ToJSON StackSpecYaml where newtype ParametersYaml = ParametersYaml { unParametersYaml :: [ParameterYaml] } + deriving stock (Eq, Show) deriving newtype ToJSON +instance Semigroup ParametersYaml where + ParametersYaml as <> ParametersYaml bs = + ParametersYaml + $ map (uncurry ParameterYaml) + $ KeyMap.toList + $ KeyMap.fromListWith (<>) + $ map (pyKey &&& pyValue) + $ bs -- flipped to make sure Last-wins + <> as + instance FromJSON ParametersYaml where parseJSON = \case Object o -> do @@ -86,32 +99,34 @@ parametersYaml :: [ParameterYaml] -> ParametersYaml parametersYaml = ParametersYaml data ParameterYaml = ParameterYaml - { _pyKey :: Key - , _pyValue :: Maybe ParameterValue + { pyKey :: Key + , pyValue :: Last ParameterValue } + deriving stock (Eq, Show) + +mkParameterYaml :: Text -> Maybe ParameterValue -> ParameterYaml +mkParameterYaml k = ParameterYaml (Key.fromText k) . Last parameterYaml :: Parameter -> Maybe ParameterYaml parameterYaml p = do k <- p ^. parameter_parameterKey - pure - $ ParameterYaml (Key.fromText k) - $ ParameterValue - <$> p - ^. parameter_parameterValue + let mv = p ^. parameter_parameterValue + pure $ mkParameterYaml k $ ParameterValue <$> mv unParameterYaml :: ParameterYaml -> Parameter unParameterYaml (ParameterYaml k v) = - makeParameter (Key.toText k) $ unParameterValue <$> v + makeParameter (Key.toText k) $ unParameterValue <$> getLast v instance FromJSON ParameterYaml where parseJSON = withObject "Parameter" $ \o -> - (ParameterYaml <$> o .: "Name" <*> o .:? "Value") - <|> (ParameterYaml <$> o .: "ParameterKey" <*> o .:? "ParameterValue") + (mkParameterYaml <$> o .: "Name" <*> o .:? "Value") + <|> (mkParameterYaml <$> o .: "ParameterKey" <*> o .:? "ParameterValue") newtype ParameterValue = ParameterValue { unParameterValue :: Text } - deriving newtype ToJSON + deriving stock (Eq, Show) + deriving newtype (Semigroup, ToJSON) instance FromJSON ParameterValue where parseJSON = \case @@ -129,8 +144,22 @@ parameterPairs (ParameterYaml k v) = [k .= v] newtype TagsYaml = TagsYaml { unTagsYaml :: [TagYaml] } + deriving stock (Eq, Show) deriving newtype ToJSON +instance Semigroup TagsYaml where + TagsYaml as <> TagsYaml bs = + TagsYaml + $ map (TagYaml . uncurry newTag) + $ HashMap.toList + $ HashMap.fromList + $ map (toPair . unTagYaml) + $ as + <> bs + where + toPair :: Tag -> (Text, Text) + toPair = (^. tag_key) &&& (^. tag_value) + instance FromJSON TagsYaml where parseJSON = \case Object o -> do @@ -149,6 +178,7 @@ tagsYaml = TagsYaml newtype TagYaml = TagYaml { unTagYaml :: Tag } + deriving newtype (Eq, Show) instance FromJSON TagYaml where parseJSON = withObject "Tag" $ \o -> do diff --git a/stackctl.cabal b/stackctl.cabal index 94ff43f..4d4e862 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -38,6 +38,8 @@ library Stackctl.ColorOption Stackctl.Colors Stackctl.Commands + Stackctl.Config + Stackctl.Config.RequiredVersion Stackctl.DirectoryOption Stackctl.FilterOption Stackctl.Options @@ -115,6 +117,7 @@ library , lens , lens-aeson , monad-logger + , mtl , optparse-applicative , resourcet , rio @@ -170,6 +173,8 @@ test-suite spec main-is: Spec.hs other-modules: Stackctl.AWS.CloudFormationSpec + Stackctl.Config.RequiredVersionSpec + Stackctl.ConfigSpec Stackctl.FilterOptionSpec Stackctl.StackDescriptionSpec Stackctl.StackSpecSpec @@ -205,8 +210,11 @@ test-suite spec TypeFamilies ghc-options: -fwrite-ide-info -Weverything -Wno-all-missed-specialisations -Wno-missing-import-lists -Wno-missing-kind-signatures -Wno-missing-local-signatures -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module -Wno-unsafe -optP-Wno-nonportable-include-path build-depends: - base ==4.* + QuickCheck + , base ==4.* + , bytestring , hspec + , mtl , stackctl , yaml default-language: Haskell2010 diff --git a/test/Stackctl/Config/RequiredVersionSpec.hs b/test/Stackctl/Config/RequiredVersionSpec.hs new file mode 100644 index 0000000..1df5ffb --- /dev/null +++ b/test/Stackctl/Config/RequiredVersionSpec.hs @@ -0,0 +1,80 @@ +module Stackctl.Config.RequiredVersionSpec + ( spec + ) where + +import Stackctl.Prelude + +import Data.Version +import Stackctl.Config.RequiredVersion +import Test.Hspec +import Test.QuickCheck + +spec :: Spec +spec = do + describe "requiredVersionFromText" $ do + it "parses with or without operator" $ do + requiredVersionFromText "1.2.3-rc1" `shouldSatisfy` isRight + requiredVersionFromText "= 1.2.3-rc1" `shouldSatisfy` isRight + + it "rejects unknown operators" $ do + requiredVersionFromText "!! 1.2.3" `shouldSatisfy` isLeft + + it "rejects invalid versions" $ do + requiredVersionFromText "= wowOMG-2/2" `shouldSatisfy` isLeft + + describe "parsing operators" $ do + let prop cmp = property . uncurry . compareAsRequiredVersion cmp + + it "compares exactly" $ prop (==) Nothing + it "compares with = " $ prop (==) $ Just "=" + it "compares with < " $ prop (<) $ Just "<" + it "compares with <=" $ prop (<=) $ Just "<=" + it "compares with > " $ prop (>) $ Just ">" + it "compares with >=" $ prop (>=) $ Just ">=" + it "compares with =~" $ prop (=~) $ Just "=~" + + + describe "=~" $ do + it "treats equal versions as satisfying" $ do + makeVersion [1, 2, 3] =~ makeVersion [1, 2, 3] `shouldBe` True + + it "treats older versions as non-satisfying" $ do + makeVersion [1, 2, 2] =~ makeVersion [1, 2, 3] `shouldBe` False + + it "treats newer versions of the same branch as satisfying" $ do + makeVersion [1, 2, 3, 1] =~ makeVersion [1, 2, 3] `shouldBe` True + + it "treats newer versions as non-satisfying" $ do + makeVersion [1, 2, 4] =~ makeVersion [1, 2, 3] `shouldBe` False + + it "respects the number of components specified" $ do + makeVersion [1, 2] =~ makeVersion [1, 2] `shouldBe` True + makeVersion [1, 2, 3] =~ makeVersion [1, 2] `shouldBe` True + makeVersion [1, 1] =~ makeVersion [1, 2] `shouldBe` False + makeVersion [1, 3] =~ makeVersion [1, 2, 3] `shouldBe` False + +compareAsRequiredVersion + :: (Version -> Version -> Bool) + -- ^ Reference compare + -> Maybe Text + -- ^ Operator + -> Version + -- ^ Hypothetical required version + -> Version + -- ^ Hypotehtical current version + -> Bool +compareAsRequiredVersion cmp mOperator required current = + runRequiredVersion mOperator required current + == Right (current `cmp` required) + +runRequiredVersion + :: Maybe Text + -- ^ Operator + -> Version + -- ^ Hypothetical required version + -> Version + -- ^ Hypothetical current version + -> Either String Bool +runRequiredVersion mOperator required current = + (`isRequiredVersionSatisfied` current) <$> requiredVersionFromText rvText + where rvText = maybe "" (<> " ") mOperator <> pack (showVersion required) diff --git a/test/Stackctl/ConfigSpec.hs b/test/Stackctl/ConfigSpec.hs new file mode 100644 index 0000000..3950f2e --- /dev/null +++ b/test/Stackctl/ConfigSpec.hs @@ -0,0 +1,75 @@ +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} + +module Stackctl.ConfigSpec + ( spec + ) where + +import Stackctl.Prelude + +import Control.Monad.Except +import qualified Data.ByteString.Char8 as BS8 +import Data.Version (showVersion) +import Paths_stackctl as Paths +import Stackctl.AWS (makeParameter, newTag) +import Stackctl.Config +import Stackctl.StackSpecYaml +import Test.Hspec + +spec :: Spec +spec = do + describe "loadConfigFromBytes" $ do + it "loads a valid config" $ do + let + result = loadConfigFromLines + [ "required_version: " <> BS8.pack (showVersion Paths.version) + , "defaults:" + , " parameters:" + , " Some: Parameter" + , " tags:" + , " Some: Tag" + ] + + case result of + Left err -> do + expectationFailure + $ "Expected to load a Config, got error: " + <> show err + Right config -> do + configParameters config + `shouldBe` Just (toParametersYaml [("Some", Just "Parameter")]) + configTags config `shouldBe` Just (toTagsYaml [("Some", "Tag")]) + + describe "applyConfig" $ do + it "defaults missing Tags" $ do + let + specYaml = StackSpecYaml + { ssyDescription = Nothing + , ssyTemplate = "" + , ssyDepends = Nothing + , ssyActions = Nothing + , ssyParameters = Nothing + , ssyCapabilities = Nothing + , ssyTags = Just $ toTagsYaml [("Hi", "There"), ("Keep", "Me")] + } + + Right config = + loadConfigFromBytes + $ "defaults:" + <> "\n tags:" + <> "\n From: Defaults" + <> "\n Keep: \"You?\"" + + Just tags = ssyTags (applyConfig config specYaml) + + tags `shouldBe` toTagsYaml + [("From", "Defaults"), ("Hi", "There"), ("Keep", "Me")] + +loadConfigFromLines :: MonadError ConfigError m => [ByteString] -> m Config +loadConfigFromLines = loadConfigFromBytes . mconcat . map (<> "\n") + +toParametersYaml :: [(Text, Maybe Text)] -> ParametersYaml +toParametersYaml = + parametersYaml . mapMaybe (parameterYaml . uncurry makeParameter) + +toTagsYaml :: [(Text, Text)] -> TagsYaml +toTagsYaml = tagsYaml . map (TagYaml . uncurry newTag) diff --git a/test/Stackctl/FilterOptionSpec.hs b/test/Stackctl/FilterOptionSpec.hs index 30402b8..2c8dcdf 100644 --- a/test/Stackctl/FilterOptionSpec.hs +++ b/test/Stackctl/FilterOptionSpec.hs @@ -8,6 +8,7 @@ import Stackctl.Prelude import Stackctl.AWS import Stackctl.AWS.Scope +import Stackctl.Config (emptyConfig) import Stackctl.FilterOption import Stackctl.StackSpec import Stackctl.StackSpecPath @@ -88,7 +89,8 @@ spec = do `shouldMatchList` ["some-name", "prefix-foo"] toSpec :: Text -> FilePath -> Maybe FilePath -> StackSpec -toSpec name path mTemplate = buildStackSpec "." specPath specBody +toSpec name path mTemplate = flip runReader emptyConfig + $ buildStackSpec "." specPath specBody where stackName = StackName name specPath = stackSpecPath scope stackName path diff --git a/test/Stackctl/StackSpecSpec.hs b/test/Stackctl/StackSpecSpec.hs index 7ceaf95..a16b3cb 100644 --- a/test/Stackctl/StackSpecSpec.hs +++ b/test/Stackctl/StackSpecSpec.hs @@ -6,6 +6,7 @@ import Stackctl.Prelude import Stackctl.AWS import Stackctl.AWS.Scope +import Stackctl.Config (emptyConfig) import Stackctl.StackSpec import Stackctl.StackSpecPath import Stackctl.StackSpecYaml @@ -27,7 +28,8 @@ spec = do `shouldBe` ["iam", "roles", "networking", "app"] toSpec :: Text -> [Text] -> StackSpec -toSpec name depends = buildStackSpec "." specPath specBody +toSpec name depends = flip runReader emptyConfig + $ buildStackSpec "." specPath specBody where stackName = StackName name specPath = stackSpecPath scope stackName "a/b.yaml" diff --git a/test/Stackctl/StackSpecYamlSpec.hs b/test/Stackctl/StackSpecYamlSpec.hs index fc9798d..030f687 100644 --- a/test/Stackctl/StackSpecYamlSpec.hs +++ b/test/Stackctl/StackSpecYamlSpec.hs @@ -116,3 +116,29 @@ spec = do Just [param] = map unParameterYaml . unParametersYaml <$> ssyParameters param ^. parameter_parameterKey `shouldBe` Just "Foo" param ^. parameter_parameterValue `shouldBe` Just "Bar" + + describe "ParametersYaml" $ do + it "has overriding Semigroup semantics" $ do + let + a = parametersYaml [] + b = parametersYaml + $ catMaybes [parameterYaml $ makeParameter "Key" (Just "B")] + c = parametersYaml + $ catMaybes [parameterYaml $ makeParameter "Key" (Just "C")] + d = parametersYaml + $ catMaybes [parameterYaml $ makeParameter "Key" Nothing] + + a <> b `shouldBe` b -- keeps keys in B + b <> c `shouldBe` c -- C overrides B (Last) + c <> d `shouldBe` c -- C overrides D (Just) + d <> c `shouldBe` c -- C overrides D (Just) + + describe "TagsYaml" $ do + it "has overriding Semigroup semantics" $ do + let + a = tagsYaml [] + b = tagsYaml [TagYaml $ newTag "Key" "B"] + c = tagsYaml [TagYaml $ newTag "Key" "C"] + + a <> b `shouldBe` b -- keeps keys in B + b <> c `shouldBe` c -- C overrides B (Last) From 81d9ccc1655f4b790a1221a98c99f5a24e8ddfb9 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Mon, 19 Dec 2022 11:40:06 -0500 Subject: [PATCH 022/187] Version bump --- CHANGELOG.md | 23 ++++++++++++++++++++++- package.yaml | 2 +- stackctl.cabal | 2 +- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2111a97..5f22a2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,25 @@ -## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.1.2.2...main) +## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.1.3.0...main) + +## [v1.1.3.0](https://github.com/freckle/stackctl/compare/v1.1.2.2...v1.1.3.0) + +- Repository-local configuration + + See https://github.com/freckle/stackctl/commit/564678203fe70b5c4c46c655dd3daeaafb6de9e0 + +- Don't duplicate re-used templates in `stackctl-cat` +- Improve `--filter` + + - Match against stack name and template, in addition to spec path. + - Automatically prepend `**/` (unless there is already a leading wildcard) and + append `{/*,.yaml,.json}` (unless there is already a trailing wildcard or + extension). + + In general, this aims to make `--filter` match more things more intuitively + for operators, but still match exactly in programmatic use-cases. + +- Various documentation improvements +- Support more natural `{Key}: {Value}` syntax in `Parameters` and `Tags` +- Fix bug where we may generate an `{}` element in `Parameters` ## [v1.1.2.2](https://github.com/freckle/stackctl/compare/v1.1.2.1...v1.1.2.2) diff --git a/package.yaml b/package.yaml index a4a8d23..773be3d 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: stackctl -version: 1.1.2.2 +version: 1.1.3.0 github: freckle/stackctl license: MIT author: Freckle Engineering diff --git a/stackctl.cabal b/stackctl.cabal index 4d4e862..f0e41f8 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -5,7 +5,7 @@ cabal-version: 1.18 -- see: https://github.com/sol/hpack name: stackctl -version: 1.1.2.2 +version: 1.1.3.0 description: Please see homepage: https://github.com/freckle/stackctl#readme bug-reports: https://github.com/freckle/stackctl/issues From a6f86141d55d15e90e045ff803afa473a9dfa7a7 Mon Sep 17 00:00:00 2001 From: Pat Brisbin Date: Tue, 3 Jan 2023 12:35:25 -0500 Subject: [PATCH 023/187] Fix JSON bugs in Spec generation * Fix invalid FromJSON(Action) We were loading the same constructor by either tag. I also moved from asum to (<|>), which produces better failures * Fix JSON instance for StackSpecYaml We didn't define a custom `ToJSON` for the `ParamtersYaml` or `TagsYaml` objects to generate the more natural key-value style, but we _did_ define custom instances on the `ParameterYaml` and `TagYaml` element types. This resulted in some weird JSON during generation or capture. * Update writeStackSpec to use ssSpecRoot It has the same values as the separate `parent` argument, which predated it. Doing this means that the `stackSpec...File` functions are only used in contexts where them being relative makes more more sense. And being relative is also required to fix `--filter`. * Add spec on filtering from the output of generate This failed until the previous commit. * Fix template use-case that still needs the root * Add some logging around ChangeSet creation * Avoid more direct accesses of StackSpec record fields --- package.yaml | 1 + src/Stackctl/AWS/CloudFormation.hs | 9 ++++-- src/Stackctl/AWS/Core.hs | 6 ++-- src/Stackctl/AWS/Scope.hs | 2 +- src/Stackctl/Action.hs | 12 ++++---- src/Stackctl/Prelude.hs | 2 +- src/Stackctl/Prompt.hs | 2 +- src/Stackctl/Spec/Generate.hs | 2 +- src/Stackctl/StackSpec.hs | 40 +++++++++++++++----------- src/Stackctl/StackSpecPath.hs | 1 + src/Stackctl/StackSpecYaml.hs | 46 ++++++++++++++++-------------- stackctl.cabal | 3 +- test/Stackctl/FilterOptionSpec.hs | 15 +++++++++- test/Stackctl/StackSpecYamlSpec.hs | 20 +++++++++++++ 14 files changed, 105 insertions(+), 56 deletions(-) diff --git a/package.yaml b/package.yaml index 773be3d..42bcc76 100644 --- a/package.yaml +++ b/package.yaml @@ -108,6 +108,7 @@ tests: source-dirs: test dependencies: - QuickCheck + - aeson - bytestring - hspec - mtl diff --git a/src/Stackctl/AWS/CloudFormation.hs b/src/Stackctl/AWS/CloudFormation.hs index 9d31d38..68cc1aa 100644 --- a/src/Stackctl/AWS/CloudFormation.hs +++ b/src/Stackctl/AWS/CloudFormation.hs @@ -115,7 +115,7 @@ newtype StackTemplate = StackTemplate { unStackTemplate :: FilePath } deriving stock (Eq, Show) - deriving newtype FromJSON + deriving newtype (FromJSON, ToJSON) data StackDeployResult = StackCreateSuccess @@ -343,10 +343,13 @@ awsCloudFormationCreateChangeSet stackName mStackDescription stackTemplate param $ trying (_ServiceError . hasStatus 400) $ do name <- newChangeSetName + + logDebug $ "Reading Template" :# ["path" .= stackTemplate] templateBody <- addStackDescription mStackDescription <$> readFileUtf8 (unStackTemplate stackTemplate) mStack <- awsCloudFormationDescribeStackMaybe stackName + let changeSetType = fromMaybe ChangeSetType_CREATE $ do stack <- mStack @@ -363,7 +366,9 @@ awsCloudFormationCreateChangeSet stackName mStackDescription stackTemplate param . (createChangeSet_capabilities ?~ capabilities) . (createChangeSet_tags ?~ tags) - logInfo "Creating changeset..." + logInfo + $ "Creating changeset..." + :# ["name" .= name, "type" .= changeSetType] csId <- awsSimple "CreateChangeSet" req (^. createChangeSetResponse_id) logDebug "Awaiting CREATE_COMPLETE" diff --git a/src/Stackctl/AWS/Core.hs b/src/Stackctl/AWS/Core.hs index 5652356..e80282c 100644 --- a/src/Stackctl/AWS/Core.hs +++ b/src/Stackctl/AWS/Core.hs @@ -1,7 +1,5 @@ module Stackctl.AWS.Core - ( - -- * AWS via 'MonadReader' - AwsEnv + ( AwsEnv , HasAwsEnv(..) , awsEnvDiscover , awsSimple @@ -111,4 +109,4 @@ awsWithin r = local $ over (awsEnvL . unL) (within r) newtype AccountId = AccountId { unAccountId :: Text } - deriving newtype (Eq, Ord, ToJSON) + deriving newtype (Eq, Ord, Show, ToJSON) diff --git a/src/Stackctl/AWS/Scope.hs b/src/Stackctl/AWS/Scope.hs index dcdd74d..49ae049 100644 --- a/src/Stackctl/AWS/Scope.hs +++ b/src/Stackctl/AWS/Scope.hs @@ -14,7 +14,7 @@ data AwsScope = AwsScope , awsAccountName :: Text , awsRegion :: Region } - deriving stock Generic + deriving stock (Eq, Show, Generic) deriving anyclass ToJSON class HasAwsScope env where diff --git a/src/Stackctl/Action.hs b/src/Stackctl/Action.hs index 61eae50..9d94494 100644 --- a/src/Stackctl/Action.hs +++ b/src/Stackctl/Action.hs @@ -31,14 +31,14 @@ data Action = Action { on :: ActionOn , run :: ActionRun } - deriving stock Generic + deriving stock (Eq, Show, Generic) deriving anyclass (FromJSON, ToJSON) newAction :: ActionOn -> ActionRun -> Action newAction = Action data ActionOn = PostDeploy - deriving stock (Eq, Generic) + deriving stock (Eq, Show, Generic) instance FromJSON ActionOn where parseJSON = withText "ActionOn" $ \case @@ -55,12 +55,12 @@ instance ToJSON ActionOn where data ActionRun = InvokeLambdaByStackOutput Text | InvokeLambdaByName Text + deriving stock (Eq, Show) instance FromJSON ActionRun where - parseJSON = withObject "ActionRun" $ \o -> asum - [ InvokeLambdaByStackOutput <$> o .: "InvokeLambdaByStackOutput" - , InvokeLambdaByStackOutput <$> o .: "InvokeLambdaByName" - ] + parseJSON = withObject "ActionRun" $ \o -> + (InvokeLambdaByStackOutput <$> o .: "InvokeLambdaByStackOutput") + <|> (InvokeLambdaByName <$> o .: "InvokeLambdaByName") instance ToJSON ActionRun where toJSON = object . \case diff --git a/src/Stackctl/Prelude.hs b/src/Stackctl/Prelude.hs index b4dd22e..b86ffcb 100644 --- a/src/Stackctl/Prelude.hs +++ b/src/Stackctl/Prelude.hs @@ -26,7 +26,7 @@ import System.FilePath as X (dropExtension, takeBaseName, takeDirectory, (<.>), ()) import UnliftIO.Directory as X (withCurrentDirectory) -{-# ANN module ("HLint: ignore Avoid restricted qualification" :: String) #-} +{-# ANN module ("HLint: ignore Avoid restricted alias" :: String) #-} decodeUtf8 :: ByteString -> Text decodeUtf8 = decodeUtf8With lenientDecode diff --git a/src/Stackctl/Prompt.hs b/src/Stackctl/Prompt.hs index 0eb25a7..d006bf5 100644 --- a/src/Stackctl/Prompt.hs +++ b/src/Stackctl/Prompt.hs @@ -41,4 +41,4 @@ promptContinue = prompt "Continue (y/n)" parse dispatch | x `elem` ["n", "N"] = Right False | otherwise = Left $ "Must be y, Y, n, or N (saw " <> x <> ")" - dispatch b = if b then pure () else exitSuccess + dispatch b = unless b exitSuccess diff --git a/src/Stackctl/Spec/Generate.hs b/src/Stackctl/Spec/Generate.hs index 4b5ca6f..09e5563 100644 --- a/src/Stackctl/Spec/Generate.hs +++ b/src/Stackctl/Spec/Generate.hs @@ -75,5 +75,5 @@ generate Generate {..} = do withThreadContext ["stackName" .= stackSpecStackName stackSpec] $ do logInfo "Generating specification" - writeStackSpec gOutputDirectory stackSpec gTemplateBody + writeStackSpec stackSpec gTemplateBody pure $ stackSpecPathFilePath specPath diff --git a/src/Stackctl/StackSpec.hs b/src/Stackctl/StackSpec.hs index 1fa5a79..4af01c4 100644 --- a/src/Stackctl/StackSpec.hs +++ b/src/Stackctl/StackSpec.hs @@ -42,6 +42,9 @@ data StackSpec = StackSpec , ssSpecBody :: StackSpecYaml } +stackSpecSpecRoot :: StackSpec -> FilePath +stackSpecSpecRoot = ssSpecRoot + stackSpecSpecPath :: StackSpec -> StackSpecPath stackSpecSpecPath = ssSpecPath @@ -60,15 +63,20 @@ stackSpecDepends = fromMaybe [] . ssyDepends . ssSpecBody stackSpecActions :: StackSpec -> [Action] stackSpecActions = fromMaybe [] . ssyActions . ssSpecBody --- | Normalized, relative path to the @[{root}/]stacks/@ file +-- | Relative path @stacks/...@ stackSpecStackFile :: StackSpec -> FilePath -stackSpecStackFile StackSpec {..} = - FilePath.normalise $ ssSpecRoot stackSpecPathFilePath ssSpecPath +stackSpecStackFile = stackSpecPathFilePath . ssSpecPath --- | Normalized, relative path to the @[{root}/]templates/@ file +-- | Relative path @templates/...@ stackSpecTemplateFile :: StackSpec -> FilePath -stackSpecTemplateFile StackSpec {..} = - FilePath.normalise $ ssSpecRoot "templates" ssyTemplate ssSpecBody +stackSpecTemplateFile = ("templates" ) . ssyTemplate . ssSpecBody + +stackSpecTemplate :: StackSpec -> StackTemplate +stackSpecTemplate spec = + StackTemplate + $ FilePath.normalise + $ ssSpecRoot spec + stackSpecTemplateFile spec stackSpecParameters :: StackSpec -> [Parameter] stackSpecParameters = @@ -127,19 +135,17 @@ writeTemplateBody path body = do dir = takeDirectory path ext = takeExtension path -writeStackSpec - :: MonadUnliftIO m - => FilePath -- ^ Parent directory - -> StackSpec - -> TemplateBody - -> m () -writeStackSpec parent stackSpec@StackSpec {..} templateBody = do +writeStackSpec :: MonadUnliftIO m => StackSpec -> TemplateBody -> m () +writeStackSpec stackSpec templateBody = do writeTemplateBody templatePath templateBody createDirectoryIfMissing True $ takeDirectory specPath - liftIO $ Yaml.encodeFile specPath ssSpecBody + liftIO $ Yaml.encodeFile specPath $ stackSpecSpecBody stackSpec where - templatePath = stackSpecTemplateFile stackSpec - specPath = parent stackSpecPathFilePath ssSpecPath + templatePath = unStackTemplate $ stackSpecTemplate stackSpec + specPath = + FilePath.normalise + $ stackSpecSpecRoot stackSpec + stackSpecStackFile stackSpec readStackSpec :: (MonadIO m, MonadReader env m, HasConfig env) @@ -168,7 +174,7 @@ createChangeSet createChangeSet spec parameters = awsCloudFormationCreateChangeSet (stackSpecStackName spec) (stackSpecStackDescription spec) - (StackTemplate $ stackSpecTemplateFile spec) + (stackSpecTemplate spec) (nubOrdOn (^. parameter_parameterKey) $ parameters <> stackSpecParameters spec ) (stackSpecCapabilities spec) diff --git a/src/Stackctl/StackSpecPath.hs b/src/Stackctl/StackSpecPath.hs index e7d0c47..bc1c970 100644 --- a/src/Stackctl/StackSpecPath.hs +++ b/src/Stackctl/StackSpecPath.hs @@ -30,6 +30,7 @@ data StackSpecPath = StackSpecPath , sspStackName :: StackName , sspPath :: FilePath } + deriving stock (Eq, Show) stackSpecPath :: AwsScope -> StackName -> FilePath -> StackSpecPath stackSpecPath sspAwsScope@AwsScope {..} sspStackName sspPath = StackSpecPath diff --git a/src/Stackctl/StackSpecYaml.hs b/src/Stackctl/StackSpecYaml.hs index d858050..ea10960 100644 --- a/src/Stackctl/StackSpecYaml.hs +++ b/src/Stackctl/StackSpecYaml.hs @@ -54,7 +54,7 @@ data StackSpecYaml = StackSpecYaml , ssyCapabilities :: Maybe [Capability] , ssyTags :: Maybe TagsYaml } - deriving stock Generic + deriving stock (Eq, Show, Generic) instance FromJSON StackSpecYaml where parseJSON = genericParseJSON $ aesonPrefix id @@ -67,7 +67,6 @@ newtype ParametersYaml = ParametersYaml { unParametersYaml :: [ParameterYaml] } deriving stock (Eq, Show) - deriving newtype ToJSON instance Semigroup ParametersYaml where ParametersYaml as <> ParametersYaml bs = @@ -95,6 +94,13 @@ instance FromJSON ParametersYaml where <> ", list of {ParameterKey, ParameterValue} Objects" <> ", or list of {Key, Value} Objects" +instance ToJSON ParametersYaml where + toJSON = object . parametersYamlPairs + toEncoding = pairs . mconcat . parametersYamlPairs + +parametersYamlPairs :: KeyValue kv => ParametersYaml -> [kv] +parametersYamlPairs = map parameterYamlPair . unParametersYaml + parametersYaml :: [ParameterYaml] -> ParametersYaml parametersYaml = ParametersYaml @@ -104,6 +110,14 @@ data ParameterYaml = ParameterYaml } deriving stock (Eq, Show) +instance FromJSON ParameterYaml where + parseJSON = withObject "Parameter" $ \o -> + (mkParameterYaml <$> o .: "Name" <*> o .:? "Value") + <|> (mkParameterYaml <$> o .: "ParameterKey" <*> o .:? "ParameterValue") + +parameterYamlPair :: KeyValue kv => ParameterYaml -> kv +parameterYamlPair ParameterYaml {..} = pyKey .= pyValue + mkParameterYaml :: Text -> Maybe ParameterValue -> ParameterYaml mkParameterYaml k = ParameterYaml (Key.fromText k) . Last @@ -117,11 +131,6 @@ unParameterYaml :: ParameterYaml -> Parameter unParameterYaml (ParameterYaml k v) = makeParameter (Key.toText k) $ unParameterValue <$> getLast v -instance FromJSON ParameterYaml where - parseJSON = withObject "Parameter" $ \o -> - (mkParameterYaml <$> o .: "Name" <*> o .:? "Value") - <|> (mkParameterYaml <$> o .: "ParameterKey" <*> o .:? "ParameterValue") - newtype ParameterValue = ParameterValue { unParameterValue :: Text } @@ -134,18 +143,10 @@ instance FromJSON ParameterValue where Number x -> pure $ ParameterValue $ dropSuffix ".0" $ pack $ show x x -> fail $ "Expected String or Number, got: " <> show x -instance ToJSON ParameterYaml where - toJSON = object . parameterPairs - toEncoding = pairs . mconcat . parameterPairs - -parameterPairs :: KeyValue a => ParameterYaml -> [a] -parameterPairs (ParameterYaml k v) = [k .= v] - newtype TagsYaml = TagsYaml { unTagsYaml :: [TagYaml] } deriving stock (Eq, Show) - deriving newtype ToJSON instance Semigroup TagsYaml where TagsYaml as <> TagsYaml bs = @@ -172,6 +173,13 @@ instance FromJSON TagsYaml where v -> typeMismatch err v where err = "Object or list of {Key, Value} Objects" +instance ToJSON TagsYaml where + toJSON = object . tagsYamlPairs + toEncoding = pairs . mconcat . tagsYamlPairs + +tagsYamlPairs :: KeyValue kv => TagsYaml -> [kv] +tagsYamlPairs = map tagYamlPair . unTagsYaml + tagsYaml :: [TagYaml] -> TagsYaml tagsYaml = TagsYaml @@ -185,12 +193,8 @@ instance FromJSON TagYaml where t <- newTag <$> o .: "Key" <*> o .: "Value" pure $ TagYaml t -instance ToJSON TagYaml where - toJSON = object . tagPairs - toEncoding = pairs . mconcat . tagPairs - -tagPairs :: KeyValue a => TagYaml -> [a] -tagPairs (TagYaml t) = ["Key" .= (t ^. tag_key), "Value" .= (t ^. tag_value)] +tagYamlPair :: KeyValue kv => TagYaml -> kv +tagYamlPair (TagYaml t) = Key.fromText (t ^. tag_key) .= (t ^. tag_value) dropSuffix :: Text -> Text -> Text dropSuffix suffix t = fromMaybe t $ T.stripSuffix suffix t diff --git a/stackctl.cabal b/stackctl.cabal index f0e41f8..72c4dd0 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -1,6 +1,6 @@ cabal-version: 1.18 --- This file has been generated from package.yaml by hpack version 0.35.0. +-- This file has been generated from package.yaml by hpack version 0.35.1. -- -- see: https://github.com/sol/hpack @@ -211,6 +211,7 @@ test-suite spec ghc-options: -fwrite-ide-info -Weverything -Wno-all-missed-specialisations -Wno-missing-import-lists -Wno-missing-kind-signatures -Wno-missing-local-signatures -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module -Wno-unsafe -optP-Wno-nonportable-include-path build-depends: QuickCheck + , aeson , base ==4.* , bytestring , hspec diff --git a/test/Stackctl/FilterOptionSpec.hs b/test/Stackctl/FilterOptionSpec.hs index 2c8dcdf..e905c5d 100644 --- a/test/Stackctl/FilterOptionSpec.hs +++ b/test/Stackctl/FilterOptionSpec.hs @@ -88,9 +88,22 @@ spec = do map specName (filterStackSpecs option specs) `shouldMatchList` ["some-name", "prefix-foo"] + describe "filterOptionFromPaths" $ do + it "finds full paths (e.g. as output by generate)" $ do + let + option = filterOptionFromPaths + $ pure "stacks/1234567890.test-account/us-east-1/stack.yaml" + specs = + [ toSpec "some-name" "stack.yaml" Nothing + , toSpec "other-path" "other-stack.yaml" $ Just "x" + ] + + map specName (filterStackSpecs option specs) + `shouldMatchList` ["some-name"] + toSpec :: Text -> FilePath -> Maybe FilePath -> StackSpec toSpec name path mTemplate = flip runReader emptyConfig - $ buildStackSpec "." specPath specBody + $ buildStackSpec ".platform/specs" specPath specBody where stackName = StackName name specPath = stackSpecPath scope stackName path diff --git a/test/Stackctl/StackSpecYamlSpec.hs b/test/Stackctl/StackSpecYamlSpec.hs index 030f687..53a4e7b 100644 --- a/test/Stackctl/StackSpecYamlSpec.hs +++ b/test/Stackctl/StackSpecYamlSpec.hs @@ -6,13 +6,33 @@ module Stackctl.StackSpecYamlSpec import Stackctl.Prelude +import Data.Aeson import qualified Data.Yaml as Yaml +import Stackctl.Action import Stackctl.AWS import Stackctl.StackSpecYaml import Test.Hspec spec :: Spec spec = do + describe "From/ToJSON" $ do + it "round trips" $ do + let + yaml = StackSpecYaml + { ssyDescription = Just $ StackDescription "Testing Stack" + , ssyTemplate = "path/to/template.yaml" + , ssyDepends = Just [StackName "a-stack", StackName "another-stack"] + , ssyActions = Just + [newAction PostDeploy $ InvokeLambdaByName "a-lambda"] + , ssyParameters = Just $ parametersYaml $ mapMaybe + parameterYaml + [makeParameter "PKey" $ Just "PValue"] + , ssyCapabilities = Just [Capability_CAPABILITY_IAM] + , ssyTags = Just $ tagsYaml [TagYaml $ newTag "TKey" "TValue"] + } + + eitherDecode (encode yaml) `shouldBe` Right yaml + describe "decoding Yaml" $ do it "reads String parameters" $ do StackSpecYaml {..} <- Yaml.decodeThrow $ mconcat From 0fb3dcd775c82a8272cc692bd50c163dc2f2dfc1 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Thu, 5 Jan 2023 08:57:05 -0500 Subject: [PATCH 024/187] Version bump --- CHANGELOG.md | 6 +++++- package.yaml | 2 +- stackctl.cabal | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f22a2e..b429e38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ -## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.1.3.0...main) +## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.1.3.1...main) + +## [v1.1.3.1](https://github.com/freckle/stackctl/compare/v1.1.3.0...v1.1.3.1) + +- Fix JSON formatting bugs in generating specification ## [v1.1.3.0](https://github.com/freckle/stackctl/compare/v1.1.2.2...v1.1.3.0) diff --git a/package.yaml b/package.yaml index 42bcc76..48d1c63 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: stackctl -version: 1.1.3.0 +version: 1.1.3.1 github: freckle/stackctl license: MIT author: Freckle Engineering diff --git a/stackctl.cabal b/stackctl.cabal index 72c4dd0..4a069a1 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -5,7 +5,7 @@ cabal-version: 1.18 -- see: https://github.com/sol/hpack name: stackctl -version: 1.1.3.0 +version: 1.1.3.1 description: Please see homepage: https://github.com/freckle/stackctl#readme bug-reports: https://github.com/freckle/stackctl/issues From dc5d0b8f669f5ee1e060f7a4ef2cf037e1891521 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Mon, 9 Jan 2023 08:22:20 -0500 Subject: [PATCH 025/187] Support --tag option in stackctl-changes/deploy Allows overriding in-file Tags, just like we can Parameters. --- doc/stackctl-changes.1.md | 5 +++++ doc/stackctl-deploy.1.md | 5 +++++ src/Stackctl/Spec/Changes.hs | 5 ++++- src/Stackctl/Spec/Deploy.hs | 5 ++++- src/Stackctl/StackSpec.hs | 5 +++-- src/Stackctl/TagOption.hs | 25 +++++++++++++++++++++++++ stackctl.cabal | 1 + 7 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 src/Stackctl/TagOption.hs diff --git a/doc/stackctl-changes.1.md b/doc/stackctl-changes.1.md index d340e6b..bac3d00 100644 --- a/doc/stackctl-changes.1.md +++ b/doc/stackctl-changes.1.md @@ -28,6 +28,11 @@ successful operation. > in overriding the Parameter as an empty string. May be specified 0 or more > times. +**\-t**, **\--tag** *\*\ + +> Override the given Tag for this operation. Omitting *VALUE* will result in +> overriding the Tag as an empty string. May be specified 0 or more times. + **PATH**\ > Write changes to **PATH**, instead of printing them. diff --git a/doc/stackctl-deploy.1.md b/doc/stackctl-deploy.1.md index 467da72..1e69cee 100644 --- a/doc/stackctl-deploy.1.md +++ b/doc/stackctl-deploy.1.md @@ -23,6 +23,11 @@ creates a Change Set and executes it after confirmation. > in overriding the Parameter as an empty string. May be specified 0 or more > times. +**\-t**, **\--tag** *\*\ + +> Override the given Tag for this operation. Omitting *VALUE* will result in +> overriding the Tag as an empty string. May be specified 0 or more times. + **\--save-change-sets** *\*\ > Save generated Change Sets to **PATH/STACK.json** diff --git a/src/Stackctl/Spec/Changes.hs b/src/Stackctl/Spec/Changes.hs index 3e40132..0e7ac3e 100644 --- a/src/Stackctl/Spec/Changes.hs +++ b/src/Stackctl/Spec/Changes.hs @@ -20,10 +20,12 @@ import Stackctl.Spec.Changes.Format 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 :: Parser ChangesOptions runChangesOptions = ChangesOptions <$> formatOption <*> many parameterOption + <*> many tagOption <*> optional (argument str ( metavar "PATH" <> help "Write changes summary to PATH" @@ -62,7 +65,7 @@ runChanges ChangesOptions {..} = do for_ specs $ \spec -> do withThreadContext ["stackName" .= stackSpecStackName spec] $ do - emChangeSet <- createChangeSet spec scoParameters + emChangeSet <- createChangeSet spec scoParameters scoTags case emChangeSet of Left err -> do diff --git a/src/Stackctl/Spec/Deploy.hs b/src/Stackctl/Spec/Deploy.hs index ce07dd4..a0c3fd0 100644 --- a/src/Stackctl/Spec/Deploy.hs +++ b/src/Stackctl/Spec/Deploy.hs @@ -23,10 +23,12 @@ import Stackctl.Prompt 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 @@ data DeployOptions = DeployOptions runDeployOptions :: Parser DeployOptions runDeployOptions = DeployOptions <$> many parameterOption + <*> many tagOption <*> optional (strOption ( long "save-change-sets" <> metavar "DIRECTORY" @@ -74,7 +77,7 @@ runDeploy DeployOptions {..} = do withThreadContext ["stackName" .= stackSpecStackName spec] $ do handleRollbackComplete sdoDeployConfirmation $ stackSpecStackName spec - emChangeSet <- createChangeSet spec sdoParameters + emChangeSet <- createChangeSet spec sdoParameters sdoTags case emChangeSet of Left err -> do diff --git a/src/Stackctl/StackSpec.hs b/src/Stackctl/StackSpec.hs index 4af01c4..f118107 100644 --- a/src/Stackctl/StackSpec.hs +++ b/src/Stackctl/StackSpec.hs @@ -170,15 +170,16 @@ createChangeSet ) => 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 diff --git a/src/Stackctl/TagOption.hs b/src/Stackctl/TagOption.hs new file mode 100644 index 0000000..5988137 --- /dev/null +++ b/src/Stackctl/TagOption.hs @@ -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 diff --git a/stackctl.cabal b/stackctl.cabal index 4a069a1..9f84516 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -59,6 +59,7 @@ library Stackctl.StackSpecPath Stackctl.StackSpecYaml Stackctl.Subcommand + Stackctl.TagOption Stackctl.VerboseOption Stackctl.Version UnliftIO.Exception.Lens From 29051b362ce99e5b63471d95f95c0654c8eeeb8f Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Mon, 9 Jan 2023 08:50:27 -0500 Subject: [PATCH 026/187] Support patterns in stack-capture --- doc/stackctl-capture.1.md | 4 ++ src/Stackctl/AWS/CloudFormation.hs | 23 +++++++++++ src/Stackctl/Spec/Capture.hs | 61 ++++++++++++++++++++---------- 3 files changed, 69 insertions(+), 19 deletions(-) diff --git a/doc/stackctl-capture.1.md b/doc/stackctl-capture.1.md index 7f5e9df..8cb953a 100644 --- a/doc/stackctl-capture.1.md +++ b/doc/stackctl-capture.1.md @@ -41,6 +41,10 @@ If files already exist at the inferred locations, they will be overwritten. **STACK**\ > Name of Stack to capture. +> +> Globs are also supported and all matching Stacks will be captured. When there +> are multiple Stacks being captured, the **--path** and **--template-path** +> will be ignored and all Stacks will be captured to their inferred paths. # ENVIRONMENT diff --git a/src/Stackctl/AWS/CloudFormation.hs b/src/Stackctl/AWS/CloudFormation.hs index 68cc1aa..a456ab4 100644 --- a/src/Stackctl/AWS/CloudFormation.hs +++ b/src/Stackctl/AWS/CloudFormation.hs @@ -1,5 +1,6 @@ module Stackctl.AWS.CloudFormation ( Stack(..) + , stack_stackName , stackDescription , stackIsRollbackComplete , StackId(..) @@ -35,6 +36,7 @@ module Stackctl.AWS.CloudFormation , awsCloudFormationDescribeStackMaybe , awsCloudFormationDescribeStackOutputs , awsCloudFormationDescribeStackEvents + , awsCloudFormationGetStackNamesMatching , awsCloudFormationGetMostRecentStackEventId , awsCloudFormationDeleteStack , awsCloudFormationWait @@ -70,6 +72,7 @@ import Amazonka.CloudFormation.DescribeStacks 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 qualified Data.UUID.V4 as UUID 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 @@ awsCloudFormationDescribeStackEvents stackName mLastId = do .| 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 ?~ activeStatuses + + 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 @@ -463,6 +483,9 @@ stackIsRollbackComplete :: Stack -> Bool stackIsRollbackComplete stack = stack ^. stack_stackStatus == StackStatus_ROLLBACK_COMPLETE +activeStatuses :: [StackStatus] +activeStatuses = [StackStatus_CREATE_COMPLETE, StackStatus_UPDATE_COMPLETE] + _ValidationError :: AsError a => Getting (First ServiceError) a ServiceError _ValidationError = _MatchServiceError defaultService "ValidationError" . hasStatus 400 diff --git a/src/Stackctl/Spec/Capture.hs b/src/Stackctl/Spec/Capture.hs index 6e4a8c3..995067f 100644 --- a/src/Stackctl/Spec/Capture.hs +++ b/src/Stackctl/Spec/Capture.hs @@ -13,6 +13,7 @@ import Stackctl.Config (HasConfig) 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 @@ data CaptureOptions = CaptureOptions , scoStackPath :: Maybe FilePath , scoDepends :: Maybe [StackName] , scoTemplateFormat :: TemplateFormat - , scoStackName :: StackName + , scoStackName :: Pattern } -- brittany-disable-next-binding @@ -54,10 +55,10 @@ runCaptureOptions = CaptureOptions ( 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 @@ runCapture -> 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 From 247da13a9c4957ab948ede65e2ad5407b1a614cb Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Mon, 9 Jan 2023 09:34:25 -0500 Subject: [PATCH 027/187] Version bump --- CHANGELOG.md | 7 ++++++- package.yaml | 2 +- stackctl.cabal | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b429e38..f785390 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/package.yaml b/package.yaml index 48d1c63..7984ad0 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: stackctl -version: 1.1.3.1 +version: 1.1.4.0 github: freckle/stackctl license: MIT author: Freckle Engineering diff --git a/stackctl.cabal b/stackctl.cabal index 9f84516..82ef6cf 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -5,7 +5,7 @@ cabal-version: 1.18 -- see: https://github.com/sol/hpack name: stackctl -version: 1.1.3.1 +version: 1.1.4.0 description: Please see homepage: https://github.com/freckle/stackctl#readme bug-reports: https://github.com/freckle/stackctl/issues From a0bf8b8854dffe7b869a435417f137a461452e24 Mon Sep 17 00:00:00 2001 From: Pat Brisbin Date: Mon, 9 Jan 2023 10:10:57 -0500 Subject: [PATCH 028/187] Update doc/stackctl-capture.1.md --- doc/stackctl-capture.1.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/stackctl-capture.1.md b/doc/stackctl-capture.1.md index 8cb953a..8ed33a5 100644 --- a/doc/stackctl-capture.1.md +++ b/doc/stackctl-capture.1.md @@ -43,7 +43,7 @@ If files already exist at the inferred locations, they will be overwritten. > Name of Stack to capture. > > Globs are also supported and all matching Stacks will be captured. When there -> are multiple Stacks being captured, the **--path** and **--template-path** +> are multiple Stacks being captured, the **\--path** and **\--template-path** > will be ignored and all Stacks will be captured to their inferred paths. # ENVIRONMENT From e8c9b520013cfd7b43c413bf16d325b971bb3871 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Mon, 9 Jan 2023 10:13:21 -0500 Subject: [PATCH 029/187] Rename status filter, add UPDATE_ROLLBACK_COMPLETE --- src/Stackctl/AWS/CloudFormation.hs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Stackctl/AWS/CloudFormation.hs b/src/Stackctl/AWS/CloudFormation.hs index a456ab4..29dbc5f 100644 --- a/src/Stackctl/AWS/CloudFormation.hs +++ b/src/Stackctl/AWS/CloudFormation.hs @@ -222,7 +222,7 @@ awsCloudFormationGetStackNamesMatching => Pattern -> m [StackName] awsCloudFormationGetStackNamesMatching p = do - let req = newListStacks & listStacks_stackStatusFilter ?~ activeStatuses + let req = newListStacks & listStacks_stackStatusFilter ?~ runningStatuses runConduit $ awsPaginate req @@ -483,8 +483,12 @@ stackIsRollbackComplete :: Stack -> Bool stackIsRollbackComplete stack = stack ^. stack_stackStatus == StackStatus_ROLLBACK_COMPLETE -activeStatuses :: [StackStatus] -activeStatuses = [StackStatus_CREATE_COMPLETE, StackStatus_UPDATE_COMPLETE] +runningStatuses :: [StackStatus] +runningStatuses = + [ StackStatus_CREATE_COMPLETE + , StackStatus_UPDATE_COMPLETE + , StackStatus_UPDATE_ROLLBACK_COMPLETE + ] _ValidationError :: AsError a => Getting (First ServiceError) a ServiceError _ValidationError = From e01adbbf374eb0119eef543dce34997408ac3803 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Mon, 9 Jan 2023 10:55:08 -0500 Subject: [PATCH 030/187] Add ENV-based configuration Adds support for the following ENV vars, which will behave as their corresponding CLI flags: - `STACKCTL_DIRECTORY` -> `--directory` - `STACKCTL_FILTERS` -> `--filters` This better supports use-cases where there is a single "setup" phase and then multiple `stackctl` calls that should all use the same values for these (e.g. GitHub Actions) without having to build the options during setup and remember to include them on all calls. The implementation relies on giving `Options` a `Semigroup` instance, with `Maybe`-wrapped fields that each respect the right-hand-side under `(<>)`. We then accept a new `Env.Parser` into `runSubcommand'` and combine the result of that such that the CLI arguments override ENV-values. The various `Has`-classes dispatch the `Maybe`, assigning defaults at that point. We further enforce going through this interface by hiding `Options` fields. This turned out to be the least-bad approach, IMO, because we can leverage semigroup semantics for combining options values from different sources, while still leaving the client-code's interface (through the `Has`-classes) untouched. A minor downside is that we can no longer use `value` and `showDefault` and the defaults no longer appear in `--help` for these two options. We do, however, retain it for `--color` and `--verbose` since those don't have corresponding ENV values. User's can set Blammo's own `LOG_*` variables (and not pass CLI arguments) if they wish to perform ENV based setup of those behaviors. --- doc/stackctl.1.md | 18 ++++++++++++- package.yaml | 2 ++ src/Stackctl/CLI.hs | 2 +- src/Stackctl/ColorOption.hs | 34 +++++++++++++++--------- src/Stackctl/DirectoryOption.hs | 28 +++++++++++++++----- src/Stackctl/FilterOption.hs | 15 +++++++++-- src/Stackctl/Options.hs | 47 ++++++++++++++++++++++++--------- src/Stackctl/Prelude.hs | 4 +++ src/Stackctl/Spec/Capture.hs | 4 +-- src/Stackctl/Spec/Cat.hs | 4 +-- src/Stackctl/Spec/Discover.hs | 4 +-- src/Stackctl/Subcommand.hs | 16 ++++++++--- src/Stackctl/VerboseOption.hs | 1 + stackctl.cabal | 2 ++ 14 files changed, 137 insertions(+), 44 deletions(-) diff --git a/doc/stackctl.1.md b/doc/stackctl.1.md index 3f77a33..fa6ea63 100644 --- a/doc/stackctl.1.md +++ b/doc/stackctl.1.md @@ -1,6 +1,6 @@ % STACKCTL(1) User Manual % -% March 2022 +% January 2023 # NAME @@ -257,6 +257,22 @@ See **stackctl-changes(1)** and **stackctl-deploy(1)**. # ENVIRONMENT +*STACKCTL_DIRECTORY*\ + +> Environment-based alternative for *\--directory*. + +*STACKCTL_FILTERS*\ + +> Environment-based alternative for *\--filters*. + +*LOG_\**\ + +> Variables such as *LOG_COLOR* or *LOG_LEVEL* will be respected by the +> underlying logging framework (Blammo). Please see its documentation for +> complete details: +> +> https://github.com/freckle/blammo#configuration + *AWS_PROFILE*\ > If set, will be used as account name in commands that create new diff --git a/package.yaml b/package.yaml index 7984ad0..8cbecd2 100644 --- a/package.yaml +++ b/package.yaml @@ -72,6 +72,7 @@ library: - cfn-flip >= 0.1.0.3 # bugfix for Condition - conduit - containers + - envparse - errors - exceptions - extra @@ -83,6 +84,7 @@ library: - optparse-applicative - resourcet - rio + - semigroups - text - time - unliftio diff --git a/src/Stackctl/CLI.hs b/src/Stackctl/CLI.hs index 32d7d6f..e7dcb54 100644 --- a/src/Stackctl/CLI.hs +++ b/src/Stackctl/CLI.hs @@ -87,7 +87,7 @@ runAppT options f = do $ defaultLogSettings logger <- newLogger $ adjustLogSettings - (options ^. colorOptionL) + (options ^. colorOptionL . to unColorOption) (options ^. verboseOptionL) envLogSettings diff --git a/src/Stackctl/ColorOption.hs b/src/Stackctl/ColorOption.hs index c423c14..dd154e0 100644 --- a/src/Stackctl/ColorOption.hs +++ b/src/Stackctl/ColorOption.hs @@ -1,5 +1,6 @@ module Stackctl.ColorOption - ( LogColor(..) + ( ColorOption(..) + , defaultColorOption , HasColorOption(..) , colorOption , colorHandle @@ -8,29 +9,38 @@ module Stackctl.ColorOption import Stackctl.Prelude import Blammo.Logging.LogSettings +import Data.Semigroup (Last(..)) import Options.Applicative +newtype ColorOption = ColorOption + { unColorOption :: LogColor + } + deriving Semigroup via Last ColorOption + +defaultColorOption :: ColorOption +defaultColorOption = ColorOption LogColorAuto + class HasColorOption env where - colorOptionL :: Lens' env LogColor + colorOptionL :: Lens' env ColorOption -instance HasColorOption LogColor where +instance HasColorOption ColorOption where colorOptionL = id -colorOption :: Parser LogColor -colorOption = option (eitherReader readLogColor) $ mconcat +colorOption :: Parser ColorOption +colorOption = option (eitherReader $ fmap ColorOption . readLogColor) $ mconcat [ long "color" , help "When to colorize output" , metavar "auto|always|never" - , value LogColorAuto - , showDefaultWith showLogColor + , value defaultColorOption + , showDefaultWith showColorOption ] -showLogColor :: LogColor -> String -showLogColor = \case +showColorOption :: ColorOption -> String +showColorOption co = case unColorOption co of LogColorAuto -> "auto" LogColorAlways -> "always" LogColorNever -> "never" -colorHandle :: MonadIO m => Handle -> LogColor -> m Bool -colorHandle h lc = shouldColorHandle settings h - where settings = setLogSettingsColor lc defaultLogSettings +colorHandle :: MonadIO m => Handle -> ColorOption -> m Bool +colorHandle h co = shouldColorHandle settings h + where settings = setLogSettingsColor (unColorOption co) defaultLogSettings diff --git a/src/Stackctl/DirectoryOption.hs b/src/Stackctl/DirectoryOption.hs index 4d588aa..b9d5e3d 100644 --- a/src/Stackctl/DirectoryOption.hs +++ b/src/Stackctl/DirectoryOption.hs @@ -1,25 +1,41 @@ module Stackctl.DirectoryOption - ( HasDirectoryOption(..) + ( DirectoryOption(..) + , defaultDirectoryOption + , HasDirectoryOption(..) + , envDirectoryOption , directoryOption ) where import Stackctl.Prelude +import Data.Semigroup (Last(..)) +import qualified Env import Options.Applicative +newtype DirectoryOption = DirectoryOption + { unDirectoryOption :: FilePath + } + deriving newtype IsString + deriving Semigroup via Last DirectoryOption + +defaultDirectoryOption :: DirectoryOption +defaultDirectoryOption = "." + class HasDirectoryOption env where - directoryOptionL :: Lens' env FilePath + directoryOptionL :: Lens' env DirectoryOption -instance HasDirectoryOption FilePath where +instance HasDirectoryOption DirectoryOption where directoryOptionL = id -directoryOption :: Parser FilePath +envDirectoryOption :: Env.Parser Env.Error DirectoryOption +envDirectoryOption = Env.var (Env.str <=< Env.nonempty) "DIRECTORY" + $ Env.help "Operate on specifications in this directory" + +directoryOption :: Parser DirectoryOption directoryOption = option str $ mconcat [ short 'd' , long "directory" , metavar "PATH" , help "Operate on specifications in PATH" - , value "." - , showDefault , action "directory" ] diff --git a/src/Stackctl/FilterOption.hs b/src/Stackctl/FilterOption.hs index 2e96565..81c5984 100644 --- a/src/Stackctl/FilterOption.hs +++ b/src/Stackctl/FilterOption.hs @@ -1,6 +1,8 @@ module Stackctl.FilterOption ( FilterOption + , defaultFilterOption , HasFilterOption(..) + , envFilterOption , filterOption , filterOptionFromPaths , filterOptionFromText @@ -10,7 +12,9 @@ module Stackctl.FilterOption import Stackctl.Prelude import qualified Data.List.NonEmpty as NE +import Data.Semigroup (Last(..)) import qualified Data.Text as T +import qualified Env import Options.Applicative import Stackctl.AWS.CloudFormation (StackName(..)) import Stackctl.StackSpec @@ -20,6 +24,7 @@ import System.FilePath.Glob newtype FilterOption = FilterOption { unFilterOption :: NonEmpty Pattern } + deriving Semigroup via Last FilterOption instance ToJSON FilterOption where toJSON = toJSON . showFilterOption @@ -31,13 +36,19 @@ class HasFilterOption env where instance HasFilterOption FilterOption where filterOptionL = id +envFilterOption :: String -> Env.Parser Env.Error FilterOption +envFilterOption items = + Env.var (first Env.UnreadError . readFilterOption) "FILTERS" + $ Env.help + $ "Filter " + <> items + <> " by patterns" + filterOption :: String -> Parser FilterOption filterOption items = option (eitherReader readFilterOption) $ mconcat [ long "filter" , metavar "PATTERN[,PATTERN]" , help $ "Filter " <> items <> " to match PATTERN(s)" - , value defaultFilterOption - , showDefaultWith showFilterOption ] filterOptionFromPaths :: NonEmpty FilePath -> FilterOption diff --git a/src/Stackctl/Options.hs b/src/Stackctl/Options.hs index c4d4a86..fcd82be 100644 --- a/src/Stackctl/Options.hs +++ b/src/Stackctl/Options.hs @@ -1,10 +1,13 @@ module Stackctl.Options - ( Options(..) + ( Options + , envParser , optionsParser ) where import Stackctl.Prelude +import Data.Semigroup.Generic +import qualified Env import Options.Applicative import Stackctl.ColorOption import Stackctl.DirectoryOption @@ -12,29 +15,49 @@ import Stackctl.FilterOption import Stackctl.VerboseOption data Options = Options - { oDirectory :: FilePath - , oFilterOption :: FilterOption - , oColor :: LogColor + { oDirectory :: Maybe DirectoryOption + , oFilter :: Maybe FilterOption + , oColor :: Maybe ColorOption , oVerbose :: Verbosity } + deriving stock Generic + deriving Semigroup via GenericSemigroupMonoid Options -instance HasDirectoryOption Options where - directoryOptionL = lens oDirectory $ \x y -> x { oDirectory = y } +directoryL :: Lens' Options (Maybe DirectoryOption) +directoryL = lens oDirectory $ \x y -> x { oDirectory = y } -instance HasColorOption Options where - colorOptionL = lens oColor $ \x y -> x { oColor = y } +filterL :: Lens' Options (Maybe FilterOption) +filterL = lens oFilter $ \x y -> x { oFilter = y } + +colorL :: Lens' Options (Maybe ColorOption) +colorL = lens oColor $ \x y -> x { oColor = y } + +instance HasDirectoryOption Options where + directoryOptionL = directoryL . maybeLens defaultDirectoryOption instance HasFilterOption Options where - filterOptionL = lens oFilterOption $ \x y -> x { oFilterOption = y } + filterOptionL = filterL . maybeLens defaultFilterOption + +instance HasColorOption Options where + colorOptionL = colorL . maybeLens defaultColorOption instance HasVerboseOption Options where verboseOptionL = lens oVerbose $ \x y -> x { oVerbose = y } -- brittany-disable-next-binding +envParser :: Env.Parser Env.Error Options +envParser = Env.prefixed "STACKCTL_" $ Options + <$> optional envDirectoryOption + <*> optional (envFilterOption "specifications") + <*> pure mempty -- use LOG_COLOR + <*> pure mempty -- use LOG_LEVEL + +-- brittany-disable-next-binding + optionsParser :: Parser Options optionsParser = Options - <$> directoryOption - <*> filterOption "specifications" - <*> colorOption + <$> optional directoryOption + <*> optional (filterOption "specifications") + <*> (Just <$> colorOption) <*> verboseOption diff --git a/src/Stackctl/Prelude.hs b/src/Stackctl/Prelude.hs index b86ffcb..307b834 100644 --- a/src/Stackctl/Prelude.hs +++ b/src/Stackctl/Prelude.hs @@ -1,6 +1,7 @@ module Stackctl.Prelude ( module X , decodeUtf8 + , maybeLens ) where import RIO as X hiding @@ -30,3 +31,6 @@ import UnliftIO.Directory as X (withCurrentDirectory) decodeUtf8 :: ByteString -> Text decodeUtf8 = decodeUtf8With lenientDecode + +maybeLens :: a -> Lens' (Maybe a) a +maybeLens x = lens (fromMaybe x) $ const Just diff --git a/src/Stackctl/Spec/Capture.hs b/src/Stackctl/Spec/Capture.hs index 995067f..8c995d9 100644 --- a/src/Stackctl/Spec/Capture.hs +++ b/src/Stackctl/Spec/Capture.hs @@ -10,7 +10,7 @@ import Options.Applicative import Stackctl.AWS import Stackctl.AWS.Scope import Stackctl.Config (HasConfig) -import Stackctl.DirectoryOption (HasDirectoryOption(..)) +import Stackctl.DirectoryOption (HasDirectoryOption(..), unDirectoryOption) import Stackctl.Spec.Generate import Stackctl.StackSpec import System.FilePath.Glob @@ -74,7 +74,7 @@ runCapture => CaptureOptions -> m () runCapture CaptureOptions {..} = do - dir <- view directoryOptionL + dir <- unDirectoryOption <$> view directoryOptionL let setScopeName scope = diff --git a/src/Stackctl/Spec/Cat.hs b/src/Stackctl/Spec/Cat.hs index 4d5d102..2b2209b 100644 --- a/src/Stackctl/Spec/Cat.hs +++ b/src/Stackctl/Spec/Cat.hs @@ -21,7 +21,7 @@ import Stackctl.AWS import Stackctl.AWS.Scope import Stackctl.Colors import Stackctl.Config (HasConfig) -import Stackctl.DirectoryOption (HasDirectoryOption(..)) +import Stackctl.DirectoryOption (HasDirectoryOption(..), unDirectoryOption) import Stackctl.FilterOption (HasFilterOption) import Stackctl.Spec.Discover import Stackctl.StackSpec @@ -67,7 +67,7 @@ runCat => CatOptions -> m () runCat CatOptions {..} = do - dir <- view directoryOptionL + dir <- unDirectoryOption <$> view directoryOptionL colors@Colors {..} <- getColorsStdout tree <- specTree <$> discoverSpecs diff --git a/src/Stackctl/Spec/Discover.hs b/src/Stackctl/Spec/Discover.hs index a109aa6..f2e27c9 100644 --- a/src/Stackctl/Spec/Discover.hs +++ b/src/Stackctl/Spec/Discover.hs @@ -10,7 +10,7 @@ import qualified Data.List.NonEmpty as NE import Stackctl.AWS import Stackctl.AWS.Scope import Stackctl.Config (HasConfig) -import Stackctl.DirectoryOption (HasDirectoryOption(..)) +import Stackctl.DirectoryOption (HasDirectoryOption(..), unDirectoryOption) import Stackctl.FilterOption (HasFilterOption(..), filterStackSpecs) import Stackctl.StackSpec import Stackctl.StackSpecPath @@ -29,7 +29,7 @@ discoverSpecs ) => m [StackSpec] discoverSpecs = do - dir <- view directoryOptionL + dir <- unDirectoryOption <$> view directoryOptionL scope@AwsScope {..} <- view awsScopeL paths <- globRelativeTo dir diff --git a/src/Stackctl/Subcommand.hs b/src/Stackctl/Subcommand.hs index 0ae9d09..f96bb03 100644 --- a/src/Stackctl/Subcommand.hs +++ b/src/Stackctl/Subcommand.hs @@ -7,6 +7,7 @@ module Stackctl.Subcommand import Stackctl.Prelude +import qualified Env import Options.Applicative import qualified Stackctl.CLI as CLI import Stackctl.Colors @@ -25,17 +26,24 @@ subcommand Subcommand {..} = command (unpack name) (run <$> withInfo description parse) runSubcommand :: Mod CommandFields (CLI.AppT (CLI.App Options) IO ()) -> IO () -runSubcommand = runSubcommand' "Work with Stack specifications" optionsParser +runSubcommand = + runSubcommand' "Work with Stack specifications" envParser optionsParser + +-- brittany-disable-next-binding runSubcommand' - :: (HasVerboseOption options, HasColorOption options) + :: (Semigroup options, HasVerboseOption options, HasColorOption options) => Text + -> Env.Parser Env.Error options -> Parser options -> Mod CommandFields (CLI.AppT (CLI.App options) IO ()) -> IO () -runSubcommand' x op sp = do - (options, act) <- execParser $ withInfo x $ (,) <$> op <*> subparser sp +runSubcommand' title parseEnv parseCLI sp = do + (options, act) <- applyEnv + <$> Env.parse (Env.header $ unpack title) parseEnv + <*> execParser (withInfo title $ (,) <$> parseCLI <*> subparser sp) CLI.runAppT options act + where applyEnv env = first (env <>) withInfo :: Text -> Parser a -> ParserInfo a withInfo d p = info (p <**> helper) $ progDesc (unpack d) <> fullDesc diff --git a/src/Stackctl/VerboseOption.hs b/src/Stackctl/VerboseOption.hs index d250233..56443b5 100644 --- a/src/Stackctl/VerboseOption.hs +++ b/src/Stackctl/VerboseOption.hs @@ -11,6 +11,7 @@ import Blammo.Logging.LogSettings.LogLevels import Options.Applicative newtype Verbosity = Verbosity [()] + deriving newtype (Semigroup, Monoid) verbositySetLogLevels :: Verbosity -> (LogSettings -> LogSettings) verbositySetLogLevels (Verbosity bs) = case bs of diff --git a/stackctl.cabal b/stackctl.cabal index 82ef6cf..10f5219 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -111,6 +111,7 @@ library , cfn-flip >=0.1.0.3 , conduit , containers + , envparse , errors , exceptions , extra @@ -122,6 +123,7 @@ library , optparse-applicative , resourcet , rio + , semigroups , text , time , unliftio From 480255ddafd865ba8cfd06ed8afe67c22ff78020 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Mon, 9 Jan 2023 15:49:12 -0500 Subject: [PATCH 031/187] Version bump --- CHANGELOG.md | 7 ++++++- package.yaml | 2 +- stackctl.cabal | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f785390..8b67a44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ -## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.1.4.0...main) +## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.2.0.0...main) + +## [v1.2.0.0](https://github.com/freckle/stackctl/compare/v1.1.3.1...v1.2.0.0) + +- Use more specific types in `Has{Directory,Filter,Color}Option` +- Add environment variable configuration for `STACKCTL_{DIRECTORY,FILTERS}` ## [v1.1.4.0](https://github.com/freckle/stackctl/compare/v1.1.3.1...v1.1.4.0) diff --git a/package.yaml b/package.yaml index 8cbecd2..ecb7520 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: stackctl -version: 1.1.4.0 +version: 1.2.0.0 github: freckle/stackctl license: MIT author: Freckle Engineering diff --git a/stackctl.cabal b/stackctl.cabal index 10f5219..a3ed41b 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -5,7 +5,7 @@ cabal-version: 1.18 -- see: https://github.com/sol/hpack name: stackctl -version: 1.1.4.0 +version: 1.2.0.0 description: Please see homepage: https://github.com/freckle/stackctl#readme bug-reports: https://github.com/freckle/stackctl/issues From 992502efab4852111831d008035c984867dce8b7 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 10 Jan 2023 15:38:02 -0500 Subject: [PATCH 032/187] Push runAppT into Subcommand{run} When all `Subcommand`s are called with `runAppT`, it meant we always had to initialize the `App` no matter the subcommand. This led to a surprising scenario where even something trivial like `stack version` requires a valid AWS connection. Oops. Making `Subcommand{run}` call `runAppT` itself is no more complicated (especially with the `runAppSubcommand` helper), in fact it makes the constraints in the `Commands` module simpler, and it naturally allows for some subcommands to _not_ do that (like `version`) and so not incur the AWS requirements. --- src/Stackctl/Commands.hs | 59 +++++++++++++++++--------------------- src/Stackctl/Subcommand.hs | 45 ++++++++++++++++++++++------- 2 files changed, 61 insertions(+), 43 deletions(-) diff --git a/src/Stackctl/Commands.hs b/src/Stackctl/Commands.hs index 32a9fe1..eb6294c 100644 --- a/src/Stackctl/Commands.hs +++ b/src/Stackctl/Commands.hs @@ -8,10 +8,7 @@ module Stackctl.Commands import Stackctl.Prelude -import Stackctl.AWS -import Stackctl.AWS.Scope import Stackctl.Colors -import Stackctl.Config (HasConfig) import Stackctl.DirectoryOption import Stackctl.FilterOption import Stackctl.Spec.Capture @@ -19,70 +16,68 @@ import Stackctl.Spec.Cat import Stackctl.Spec.Changes import Stackctl.Spec.Deploy import Stackctl.Subcommand +import Stackctl.VerboseOption import Stackctl.Version cat - :: ( HasLogger env - , HasAwsScope env - , HasConfig env - , HasDirectoryOption env - , HasFilterOption env - , HasColorOption env + :: ( HasColorOption options + , HasVerboseOption options + , HasDirectoryOption options + , HasFilterOption options ) - => Subcommand CatOptions env + => Subcommand options CatOptions cat = Subcommand { name = "cat" , description = "Pretty-print specifications" , parse = runCatOptions - , run = runCat + , run = runAppSubcommand runCat } capture - :: (HasAwsScope env, HasAwsEnv env, HasConfig env, HasDirectoryOption env) - => Subcommand CaptureOptions env + :: ( HasColorOption options + , HasVerboseOption options + , HasDirectoryOption options + ) + => Subcommand options CaptureOptions capture = Subcommand { name = "capture" , description = "Capture deployed Stacks as specifications" , parse = runCaptureOptions - , run = runCapture + , run = runAppSubcommand runCapture } changes - :: ( HasLogger env - , HasAwsScope env - , HasAwsEnv env - , HasConfig env - , HasDirectoryOption env - , HasFilterOption env + :: ( HasColorOption options + , HasVerboseOption options + , HasDirectoryOption options + , HasFilterOption options ) - => Subcommand ChangesOptions env + => Subcommand options ChangesOptions changes = Subcommand { name = "changes" , description = "Review changes between specification and deployed state" , parse = runChangesOptions - , run = runChanges + , run = runAppSubcommand runChanges } deploy - :: ( HasLogger env - , HasAwsScope env - , HasAwsEnv env - , HasConfig env - , HasDirectoryOption env - , HasFilterOption env + :: ( HasColorOption options + , HasVerboseOption options + , HasDirectoryOption options + , HasFilterOption options ) - => Subcommand DeployOptions env + => Subcommand options DeployOptions deploy = Subcommand { name = "deploy" , description = "Deploy specifications" , parse = runDeployOptions - , run = runDeploy + , run = runAppSubcommand runDeploy } -version :: Subcommand () env +version :: Subcommand options () version = Subcommand { name = "version" , description = "Output the version" , parse = pure () - , run = \() -> logVersion + , run = \() _ -> logVersion } diff --git a/src/Stackctl/Subcommand.hs b/src/Stackctl/Subcommand.hs index f96bb03..928c9e4 100644 --- a/src/Stackctl/Subcommand.hs +++ b/src/Stackctl/Subcommand.hs @@ -3,47 +3,70 @@ module Stackctl.Subcommand , subcommand , runSubcommand , runSubcommand' + , runAppSubcommand ) where import Stackctl.Prelude import qualified Env import Options.Applicative -import qualified Stackctl.CLI as CLI -import Stackctl.Colors +import Stackctl.CLI +import Stackctl.ColorOption import Stackctl.Options import Stackctl.VerboseOption -data Subcommand options env = Subcommand +data Subcommand options subOptions = Subcommand { name :: Text , description :: Text - , parse :: Parser options - , run :: options -> CLI.AppT env IO () + , parse :: Parser subOptions + , run :: subOptions -> options -> IO () } -subcommand :: Subcommand options env -> Mod CommandFields (CLI.AppT env IO ()) +subcommand + :: Subcommand options subOptions -> Mod CommandFields (options -> IO ()) subcommand Subcommand {..} = command (unpack name) (run <$> withInfo description parse) -runSubcommand :: Mod CommandFields (CLI.AppT (CLI.App Options) IO ()) -> IO () +runSubcommand :: Mod CommandFields (Options -> IO a) -> IO a runSubcommand = runSubcommand' "Work with Stack specifications" envParser optionsParser -- brittany-disable-next-binding runSubcommand' - :: (Semigroup options, HasVerboseOption options, HasColorOption options) + :: Semigroup options => Text -> Env.Parser Env.Error options -> Parser options - -> Mod CommandFields (CLI.AppT (CLI.App options) IO ()) - -> IO () + -> Mod CommandFields (options -> IO a) + -> IO a runSubcommand' title parseEnv parseCLI sp = do (options, act) <- applyEnv <$> Env.parse (Env.header $ unpack title) parseEnv <*> execParser (withInfo title $ (,) <$> parseCLI <*> subparser sp) - CLI.runAppT options act + + act options where applyEnv env = first (env <>) +-- | Use this in the 'run' member of a 'Subcommand' that wants 'AppT' +-- +-- @ +-- -- ... +-- , parse = parseFooOptions +-- , run = 'runAppSubcommand' runFoo +-- } +-- +-- runFoo :: (MonadReader env m, HasAws env) => FooOptions -> m () +-- runFoo = undefined +-- @ +-- +runAppSubcommand + :: (HasColorOption options, HasVerboseOption options) + => (subOptions -> AppT (App options) IO a) + -> subOptions + -> options + -> IO a +runAppSubcommand f subOptions options = runAppT options $ f subOptions + withInfo :: Text -> Parser a -> ParserInfo a withInfo d p = info (p <**> helper) $ progDesc (unpack d) <> fullDesc From 46e5beb7b69da3ee04d5a1d5043a2111341a4437 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 10 Jan 2023 15:42:47 -0500 Subject: [PATCH 033/187] Fix incorrect run-vs-parse naming --- src/Stackctl/Commands.hs | 8 ++++---- src/Stackctl/Spec/Capture.hs | 6 +++--- src/Stackctl/Spec/Cat.hs | 6 +++--- src/Stackctl/Spec/Changes.hs | 6 +++--- src/Stackctl/Spec/Deploy.hs | 6 +++--- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Stackctl/Commands.hs b/src/Stackctl/Commands.hs index eb6294c..ed3afd1 100644 --- a/src/Stackctl/Commands.hs +++ b/src/Stackctl/Commands.hs @@ -29,7 +29,7 @@ cat cat = Subcommand { name = "cat" , description = "Pretty-print specifications" - , parse = runCatOptions + , parse = parseCatOptions , run = runAppSubcommand runCat } @@ -42,7 +42,7 @@ capture capture = Subcommand { name = "capture" , description = "Capture deployed Stacks as specifications" - , parse = runCaptureOptions + , parse = parseCaptureOptions , run = runAppSubcommand runCapture } @@ -56,7 +56,7 @@ changes changes = Subcommand { name = "changes" , description = "Review changes between specification and deployed state" - , parse = runChangesOptions + , parse = parseChangesOptions , run = runAppSubcommand runChanges } @@ -70,7 +70,7 @@ deploy deploy = Subcommand { name = "deploy" , description = "Deploy specifications" - , parse = runDeployOptions + , parse = parseDeployOptions , run = runAppSubcommand runDeploy } diff --git a/src/Stackctl/Spec/Capture.hs b/src/Stackctl/Spec/Capture.hs index 8c995d9..b528fe7 100644 --- a/src/Stackctl/Spec/Capture.hs +++ b/src/Stackctl/Spec/Capture.hs @@ -1,6 +1,6 @@ module Stackctl.Spec.Capture ( CaptureOptions(..) - , runCaptureOptions + , parseCaptureOptions , runCapture ) where @@ -26,8 +26,8 @@ data CaptureOptions = CaptureOptions -- brittany-disable-next-binding -runCaptureOptions :: Parser CaptureOptions -runCaptureOptions = CaptureOptions +parseCaptureOptions :: Parser CaptureOptions +parseCaptureOptions = CaptureOptions <$> optional (strOption ( short 'n' <> long "account-name" diff --git a/src/Stackctl/Spec/Cat.hs b/src/Stackctl/Spec/Cat.hs index 2b2209b..c72c4c2 100644 --- a/src/Stackctl/Spec/Cat.hs +++ b/src/Stackctl/Spec/Cat.hs @@ -1,6 +1,6 @@ module Stackctl.Spec.Cat ( CatOptions(..) - , runCatOptions + , parseCatOptions , runCat ) where @@ -36,8 +36,8 @@ data CatOptions = CatOptions -- brittany-disable-next-binding -runCatOptions :: Parser CatOptions -runCatOptions = CatOptions +parseCatOptions :: Parser CatOptions +parseCatOptions = CatOptions <$> switch ( long "no-stacks" <> help "Only show templates/" diff --git a/src/Stackctl/Spec/Changes.hs b/src/Stackctl/Spec/Changes.hs index 0e7ac3e..612adb4 100644 --- a/src/Stackctl/Spec/Changes.hs +++ b/src/Stackctl/Spec/Changes.hs @@ -1,6 +1,6 @@ module Stackctl.Spec.Changes ( ChangesOptions(..) - , runChangesOptions + , parseChangesOptions , runChanges ) where @@ -31,8 +31,8 @@ data ChangesOptions = ChangesOptions -- brittany-disable-next-binding -runChangesOptions :: Parser ChangesOptions -runChangesOptions = ChangesOptions +parseChangesOptions :: Parser ChangesOptions +parseChangesOptions = ChangesOptions <$> formatOption <*> many parameterOption <*> many tagOption diff --git a/src/Stackctl/Spec/Deploy.hs b/src/Stackctl/Spec/Deploy.hs index a0c3fd0..4857e4a 100644 --- a/src/Stackctl/Spec/Deploy.hs +++ b/src/Stackctl/Spec/Deploy.hs @@ -1,7 +1,7 @@ module Stackctl.Spec.Deploy ( DeployOptions(..) , DeployConfirmation(..) - , runDeployOptions + , parseDeployOptions , runDeploy ) where @@ -36,8 +36,8 @@ data DeployOptions = DeployOptions -- brittany-disable-next-binding -runDeployOptions :: Parser DeployOptions -runDeployOptions = DeployOptions +parseDeployOptions :: Parser DeployOptions +parseDeployOptions = DeployOptions <$> many parameterOption <*> many tagOption <*> optional (strOption From d63b338a645ea18f03393e63e25729757d12600d Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 10 Jan 2023 15:45:57 -0500 Subject: [PATCH 034/187] Use make.install in the release workflow This also ensures the archive we built is correct (can install). We weren't doing this before because calling `stackctl version` required a valid environment for full `App` initialization; that's no longer the case. --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bc3b2b8..f091f2e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -57,7 +57,7 @@ jobs: - uses: r-lib/actions/setup-pandoc@v2 - if: ${{ runner.os == 'macOS' }} run: brew install coreutils # need GNU install - - run: make dist/stackctl.tar.gz PANDOC=pandoc + - run: make install.check PANDOC=pandoc - uses: actions/upload-release-asset@v1 id: upload-release-asset env: From faa65e2fa058a658406a2408af4892c208f78648 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 10 Jan 2023 17:03:48 -0500 Subject: [PATCH 035/187] Remove long-standing TODO in ci.yml --- .github/workflows/ci.yml | 36 +----------------------------------- 1 file changed, 1 insertion(+), 35 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc515cf..85ca0ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,41 +11,7 @@ jobs: steps: - uses: actions/checkout@v3 - uses: freckle/stack-cache-action@v2 - - - id: stack - uses: freckle/stack-action@v3 - with: - stack-arguments: --copy-bins - - - uses: actions/upload-artifact@v3 - with: - name: stackctl - path: | - ${{ steps.stack.outputs.local-bin }}/stackctl - - # TODO - # integration-tests: - # needs: build - # runs-on: ubuntu-latest - # environment: development - # permissions: - # id-token: write - # contents: read - - # steps: - # - uses: aws-actions/configure-aws-credentials@v1 - # with: - # role-to-assume: ${{ secrets.AWS_ROLE_ARN }} - # aws-region: us-east-1 - # - uses: actions/checkout@v2 - # - uses: actions/setup-python@v2 - # - uses: actions/download-artifact@v2 - # with: - # name: stackctl - # path: /usr/local/bin - # - run: sudo chmod +x /usr/local/bin/stackctl - # - run: pip install cram - # - run: cram -v integration/tests + - uses: freckle/stack-action@v3 lint: runs-on: ubuntu-20.04 From 6d8e731c2db8efa6c0bf4ccbfe1892793b1143e9 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 11 Jan 2023 11:01:38 -0500 Subject: [PATCH 036/187] Version bump --- CHANGELOG.md | 9 ++++++++- package.yaml | 2 +- stackctl.cabal | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b67a44..87e43f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,11 @@ -## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.2.0.0...main) +## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.3.0.0...main) + +## [v1.3.0.0](https://github.com/freckle/stackctl/compare/v1.2.0.1...v1.3.0.0) + +- Fix it so commands like `version` don't need a valid AWS environment + + This changes the `Subcommand` interface and so is a major version update for + the purposes of those using Stackctl as a library. ## [v1.2.0.0](https://github.com/freckle/stackctl/compare/v1.1.3.1...v1.2.0.0) diff --git a/package.yaml b/package.yaml index ecb7520..587cf0d 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: stackctl -version: 1.2.0.0 +version: 1.3.0.0 github: freckle/stackctl license: MIT author: Freckle Engineering diff --git a/stackctl.cabal b/stackctl.cabal index a3ed41b..1ca1336 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -5,7 +5,7 @@ cabal-version: 1.18 -- see: https://github.com/sol/hpack name: stackctl -version: 1.2.0.0 +version: 1.3.0.0 description: Please see homepage: https://github.com/freckle/stackctl#readme bug-reports: https://github.com/freckle/stackctl/issues From 25c6d4356010c6d3a04aa9b9f560cdc669f7c03f Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 11 Jan 2023 08:07:18 -0500 Subject: [PATCH 037/187] Refactor RequiredVersion, add instances https://app.asana.com/0/13211253278157/1203652238195585/f We need `Eq` to use `RequiredVersion` in tests. And we need `ToJSON` for structured logging. The current `Show` instance was custom and doesn't render a valid Haskell syntax, which is an anti-pattern. The reason for this is that the `requiredVersionCompare` member was itself a function, so we couldn't just derive a `stock` `Show`. By removing that member, and instead holding a new `RequiredVersionOp` enumeration that can be converted to it, we can now derive `stock` `Eq` and `Show`. The new `requiredVersionOpToText` and `requiredVersionOpCompare` functions will need to be kept in sync, but I don't see them changing very frequently (and we have great tests here). We retain the more human-readable format in the new `ToJSON` instance, because we need it in `FromJSON` (to parse Yaml correctly) and it must of course round-trip with `ToJSON`. --- src/Stackctl/Config/RequiredVersion.hs | 84 ++++++++++++++++++-------- 1 file changed, 59 insertions(+), 25 deletions(-) diff --git a/src/Stackctl/Config/RequiredVersion.hs b/src/Stackctl/Config/RequiredVersion.hs index 8aae43e..b7a627d 100644 --- a/src/Stackctl/Config/RequiredVersion.hs +++ b/src/Stackctl/Config/RequiredVersion.hs @@ -1,5 +1,6 @@ module Stackctl.Config.RequiredVersion ( RequiredVersion(..) + , requiredVersionToText , requiredVersionFromText , isRequiredVersionSatisfied @@ -18,19 +19,24 @@ import qualified Data.Version as Version import Text.ParserCombinators.ReadP (readP_to_S) data RequiredVersion = RequiredVersion - { requiredVersionOp :: Text - , requiredVersionCompare :: Version -> Version -> Bool + { requiredVersionOp :: RequiredVersionOp , requiredVersionCompareWith :: Version } - -instance Show RequiredVersion where - show RequiredVersion {..} = - unpack requiredVersionOp <> " " <> showVersion requiredVersionCompareWith + deriving stock (Eq, Show) instance FromJSON RequiredVersion where parseJSON = withText "RequiredVersion" $ either fail pure . requiredVersionFromText +instance ToJSON RequiredVersion where + toJSON = toJSON . requiredVersionToText + toEncoding = toEncoding . requiredVersionToText + +requiredVersionToText :: RequiredVersion -> Text +requiredVersionToText RequiredVersion {..} = + requiredVersionOpToText requiredVersionOp <> " " <> pack + (showVersion requiredVersionCompareWith) + requiredVersionFromText :: Text -> Either String RequiredVersion requiredVersionFromText = fromWords . T.words where @@ -44,21 +50,21 @@ requiredVersionFromText = fromWords . T.words <> " did not parse as optional operator and version string" parseRequiredVersion :: Text -> Text -> Either String RequiredVersion - parseRequiredVersion op w = do - v <- parseVersion w - - case op of - "=" -> Right $ RequiredVersion op (==) v - "<" -> Right $ RequiredVersion op (<) v - "<=" -> Right $ RequiredVersion op (<=) v - ">" -> Right $ RequiredVersion op (>) v - ">=" -> Right $ RequiredVersion op (>=) v - "=~" -> Right $ RequiredVersion op (=~) v - _ -> - Left - $ "Invalid comparison operator (" - <> unpack op - <> "), may only be =, <, <=, >, >=, or =~" + parseRequiredVersion op w = RequiredVersion <$> parseOp op <*> parseVersion w + + parseOp :: Text -> Either String RequiredVersionOp + parseOp op = case op of + "=" -> Right RequiredVersionEQ + "<" -> Right RequiredVersionLT + "<=" -> Right RequiredVersionLTE + ">" -> Right RequiredVersionGT + ">=" -> Right RequiredVersionGTE + "=~" -> Right RequiredVersionIsh + _ -> + Left + $ "Invalid comparison operator (" + <> unpack op + <> "), may only be =, <, <=, >, >=, or =~" parseVersion :: Text -> Either String Version parseVersion t = @@ -68,6 +74,38 @@ requiredVersionFromText = fromWords . T.words $ readP_to_S Version.parseVersion s where s = unpack t +isRequiredVersionSatisfied :: RequiredVersion -> Version -> Bool +isRequiredVersionSatisfied RequiredVersion {..} = + (`requiredVersionCompare` requiredVersionCompareWith) + where requiredVersionCompare = requiredVersionOpCompare requiredVersionOp + +data RequiredVersionOp + = RequiredVersionEQ + | RequiredVersionLT + | RequiredVersionLTE + | RequiredVersionGT + | RequiredVersionGTE + | RequiredVersionIsh + deriving stock (Eq, Show) + +requiredVersionOpToText :: RequiredVersionOp -> Text +requiredVersionOpToText = \case + RequiredVersionEQ -> "==" + RequiredVersionLT -> "<" + RequiredVersionLTE -> "<=" + RequiredVersionGT -> ">" + RequiredVersionGTE -> ">=" + RequiredVersionIsh -> "=~" + +requiredVersionOpCompare :: RequiredVersionOp -> Version -> Version -> Bool +requiredVersionOpCompare = \case + RequiredVersionEQ -> (==) + RequiredVersionLT -> (<) + RequiredVersionLTE -> (<=) + RequiredVersionGT -> (>) + RequiredVersionGTE -> (>=) + RequiredVersionIsh -> (=~) + (=~) :: Version -> Version -> Bool a =~ b = a >= b && a < incrementVersion b where @@ -75,7 +113,3 @@ a =~ b = a >= b && a < incrementVersion b onVersion f = makeVersion . f . versionBranch backwards f = reverse . f . reverse onHead f as = maybe as (uncurry (:) . first f) $ uncons as - -isRequiredVersionSatisfied :: RequiredVersion -> Version -> Bool -isRequiredVersionSatisfied RequiredVersion {..} = - (`requiredVersionCompare` requiredVersionCompareWith) From 4432c1f49509eeba20b4bd28e890a9b6a4f228fc Mon Sep 17 00:00:00 2001 From: Pat Brisbin Date: Thu, 12 Jan 2023 11:18:27 -0500 Subject: [PATCH 038/187] Update src/Stackctl/Config/RequiredVersion.hs --- src/Stackctl/Config/RequiredVersion.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Stackctl/Config/RequiredVersion.hs b/src/Stackctl/Config/RequiredVersion.hs index b7a627d..0bae8db 100644 --- a/src/Stackctl/Config/RequiredVersion.hs +++ b/src/Stackctl/Config/RequiredVersion.hs @@ -53,14 +53,14 @@ requiredVersionFromText = fromWords . T.words parseRequiredVersion op w = RequiredVersion <$> parseOp op <*> parseVersion w parseOp :: Text -> Either String RequiredVersionOp - parseOp op = case op of + parseOp = \case "=" -> Right RequiredVersionEQ "<" -> Right RequiredVersionLT "<=" -> Right RequiredVersionLTE ">" -> Right RequiredVersionGT ">=" -> Right RequiredVersionGTE "=~" -> Right RequiredVersionIsh - _ -> + op -> Left $ "Invalid comparison operator (" <> unpack op From e0b946e1719651b899c6bc0cb28c6eb03f4bad20 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Thu, 12 Jan 2023 11:38:05 -0500 Subject: [PATCH 039/187] Add QuickCheck test on JSON round-tripping --- package.yaml | 1 + src/Stackctl/Config/RequiredVersion.hs | 10 +++++++++- stackctl.cabal | 1 + test/Stackctl/Config/RequiredVersionSpec.hs | 5 +++++ 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/package.yaml b/package.yaml index 587cf0d..d0b8afd 100644 --- a/package.yaml +++ b/package.yaml @@ -59,6 +59,7 @@ library: dependencies: - Blammo >= 1.1.1.1 # pushLoggerLn, getLoggerShouldColor - Glob + - QuickCheck - aeson - aeson-casing - aeson-pretty diff --git a/src/Stackctl/Config/RequiredVersion.hs b/src/Stackctl/Config/RequiredVersion.hs index 0bae8db..7753232 100644 --- a/src/Stackctl/Config/RequiredVersion.hs +++ b/src/Stackctl/Config/RequiredVersion.hs @@ -1,5 +1,6 @@ module Stackctl.Config.RequiredVersion ( RequiredVersion(..) + , RequiredVersionOp(..) , requiredVersionToText , requiredVersionFromText , isRequiredVersionSatisfied @@ -16,6 +17,7 @@ import qualified Data.List.NonEmpty as NE import qualified Data.Text as T import Data.Version hiding (parseVersion) import qualified Data.Version as Version +import Test.QuickCheck import Text.ParserCombinators.ReadP (readP_to_S) data RequiredVersion = RequiredVersion @@ -24,6 +26,9 @@ data RequiredVersion = RequiredVersion } deriving stock (Eq, Show) +instance Arbitrary RequiredVersion where + arbitrary = RequiredVersion <$> arbitrary <*> arbitrary + instance FromJSON RequiredVersion where parseJSON = withText "RequiredVersion" $ either fail pure . requiredVersionFromText @@ -86,7 +91,10 @@ data RequiredVersionOp | RequiredVersionGT | RequiredVersionGTE | RequiredVersionIsh - deriving stock (Eq, Show) + deriving stock (Eq, Show, Bounded, Enum) + +instance Arbitrary RequiredVersionOp where + arbitrary = arbitraryBoundedEnum requiredVersionOpToText :: RequiredVersionOp -> Text requiredVersionOpToText = \case diff --git a/stackctl.cabal b/stackctl.cabal index 1ca1336..717d785 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -97,6 +97,7 @@ library build-depends: Blammo >=1.1.1.1 , Glob + , QuickCheck , aeson , aeson-casing , aeson-pretty diff --git a/test/Stackctl/Config/RequiredVersionSpec.hs b/test/Stackctl/Config/RequiredVersionSpec.hs index 1df5ffb..bdd7c92 100644 --- a/test/Stackctl/Config/RequiredVersionSpec.hs +++ b/test/Stackctl/Config/RequiredVersionSpec.hs @@ -4,6 +4,7 @@ module Stackctl.Config.RequiredVersionSpec import Stackctl.Prelude +import Data.Aeson (decode, encode) import Data.Version import Stackctl.Config.RequiredVersion import Test.Hspec @@ -11,6 +12,10 @@ import Test.QuickCheck spec :: Spec spec = do + describe "JSON" $ do + it "round-trips" $ property $ \rv -> do + decode (encode @RequiredVersion rv) `shouldBe` Just rv + describe "requiredVersionFromText" $ do it "parses with or without operator" $ do requiredVersionFromText "1.2.3-rc1" `shouldSatisfy` isRight From d5d870eaaa6a95f8247bf991b52903de62a0536b Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Thu, 12 Jan 2023 11:38:18 -0500 Subject: [PATCH 040/187] Also parse "==" in required_version This seems natural, and was what I was printing in the to-text function anyway. Let's be permissive in what we accept. --- src/Stackctl/Config/RequiredVersion.hs | 1 + test/Stackctl/Config/RequiredVersionSpec.hs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Stackctl/Config/RequiredVersion.hs b/src/Stackctl/Config/RequiredVersion.hs index 7753232..930d7b5 100644 --- a/src/Stackctl/Config/RequiredVersion.hs +++ b/src/Stackctl/Config/RequiredVersion.hs @@ -60,6 +60,7 @@ requiredVersionFromText = fromWords . T.words parseOp :: Text -> Either String RequiredVersionOp parseOp = \case "=" -> Right RequiredVersionEQ + "==" -> Right RequiredVersionEQ "<" -> Right RequiredVersionLT "<=" -> Right RequiredVersionLTE ">" -> Right RequiredVersionGT diff --git a/test/Stackctl/Config/RequiredVersionSpec.hs b/test/Stackctl/Config/RequiredVersionSpec.hs index bdd7c92..27e2475 100644 --- a/test/Stackctl/Config/RequiredVersionSpec.hs +++ b/test/Stackctl/Config/RequiredVersionSpec.hs @@ -32,6 +32,7 @@ spec = do it "compares exactly" $ prop (==) Nothing it "compares with = " $ prop (==) $ Just "=" + it "compares with ==" $ prop (==) $ Just "==" it "compares with < " $ prop (<) $ Just "<" it "compares with <=" $ prop (<=) $ Just "<=" it "compares with > " $ prop (>) $ Just ">" From f10c6ba650a44a5ba77bc860c867f53cb91780d6 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Fri, 13 Jan 2023 11:06:57 -0500 Subject: [PATCH 041/187] Respect LOG_COLOR Within Stackctl, the `--color` option was always present with a (possibly default) value, and always applied to the `LogSettings` for Blammo. This meant that `LOG_COLOR` was not being respected. This is a problem for our desired CI setup, where we'd like to establish configuration through environment variables in a single step, to impact later calls to `stackctl` (or through `platform`). By moving the option to a `Maybe`, and only applying it when present, we will ensure `LOG_COLOR` is respected. This simplified a lot generally too, which is nice. It's also following the same pattern as other ENV-or-option settings. The only downside is that of the pattern overall, we can't use `value` in the options parser, so `--help` won't display defaults. This is acceptable because we document in the man-pages anyway. --- src/Stackctl/CLI.hs | 8 +++++--- src/Stackctl/ColorOption.hs | 27 ++------------------------- src/Stackctl/Colors.hs | 13 +++++-------- src/Stackctl/Commands.hs | 2 +- src/Stackctl/Options.hs | 7 ++----- src/Stackctl/Spec/Cat.hs | 1 - 6 files changed, 15 insertions(+), 43 deletions(-) diff --git a/src/Stackctl/CLI.hs b/src/Stackctl/CLI.hs index e7dcb54..efd5d2c 100644 --- a/src/Stackctl/CLI.hs +++ b/src/Stackctl/CLI.hs @@ -87,7 +87,7 @@ runAppT options f = do $ defaultLogSettings logger <- newLogger $ adjustLogSettings - (options ^. colorOptionL . to unColorOption) + (options ^. colorOptionL) (options ^. verboseOptionL) envLogSettings @@ -115,5 +115,7 @@ runAppT options f = do $ withThreadContext context $ unAppT f -adjustLogSettings :: LogColor -> Verbosity -> LogSettings -> LogSettings -adjustLogSettings lc v = setLogSettingsColor lc . verbositySetLogLevels v +adjustLogSettings + :: Maybe ColorOption -> Verbosity -> LogSettings -> LogSettings +adjustLogSettings mco v = + maybe id (setLogSettingsColor . unColorOption) mco . verbositySetLogLevels v diff --git a/src/Stackctl/ColorOption.hs b/src/Stackctl/ColorOption.hs index dd154e0..6785ccd 100644 --- a/src/Stackctl/ColorOption.hs +++ b/src/Stackctl/ColorOption.hs @@ -1,9 +1,7 @@ module Stackctl.ColorOption ( ColorOption(..) - , defaultColorOption , HasColorOption(..) , colorOption - , colorHandle ) where import Stackctl.Prelude @@ -17,30 +15,9 @@ newtype ColorOption = ColorOption } deriving Semigroup via Last ColorOption -defaultColorOption :: ColorOption -defaultColorOption = ColorOption LogColorAuto - class HasColorOption env where - colorOptionL :: Lens' env ColorOption - -instance HasColorOption ColorOption where - colorOptionL = id + colorOptionL :: Lens' env (Maybe ColorOption) colorOption :: Parser ColorOption colorOption = option (eitherReader $ fmap ColorOption . readLogColor) $ mconcat - [ long "color" - , help "When to colorize output" - , metavar "auto|always|never" - , value defaultColorOption - , showDefaultWith showColorOption - ] - -showColorOption :: ColorOption -> String -showColorOption co = case unColorOption co of - LogColorAuto -> "auto" - LogColorAlways -> "always" - LogColorNever -> "never" - -colorHandle :: MonadIO m => Handle -> ColorOption -> m Bool -colorHandle h co = shouldColorHandle settings h - where settings = setLogSettingsColor (unColorOption co) defaultLogSettings + [long "color", help "When to colorize output", metavar "auto|always|never"] diff --git a/src/Stackctl/Colors.hs b/src/Stackctl/Colors.hs index 3123e44..14b50cd 100644 --- a/src/Stackctl/Colors.hs +++ b/src/Stackctl/Colors.hs @@ -1,7 +1,6 @@ -- | Facilities for colorizing output module Stackctl.Colors ( Colors(..) - , HasColorOption , getColorsStdout , getColorsLogger , noColors @@ -11,20 +10,18 @@ import Stackctl.Prelude import Blammo.Logging.Colors import Blammo.Logging.Logger -import Stackctl.ColorOption (HasColorOption(..), colorHandle) +import Blammo.Logging.LogSettings (shouldColorHandle) -- | Return 'Colors' based on options and 'stdout' -getColorsStdout - :: (MonadIO m, MonadReader env m, HasColorOption env) => m Colors +getColorsStdout :: (MonadIO m, MonadReader env m, HasLogger env) => m Colors getColorsStdout = getColorsHandle stdout -- | Return 'Colors' based on options given 'Handle' getColorsHandle - :: (MonadIO m, MonadReader env m, HasColorOption env) => Handle -> m Colors + :: (MonadIO m, MonadReader env m, HasLogger env) => Handle -> m Colors getColorsHandle h = do - colorOption <- view colorOptionL - c <- colorHandle h colorOption - pure $ getColors c + ls <- view $ loggerL . to getLoggerLogSettings + getColors <$> shouldColorHandle ls h -- | Return 'Colors' consistent with the ambient 'Logger' getColorsLogger :: (MonadReader env m, HasLogger env) => m Colors diff --git a/src/Stackctl/Commands.hs b/src/Stackctl/Commands.hs index ed3afd1..2d07e5b 100644 --- a/src/Stackctl/Commands.hs +++ b/src/Stackctl/Commands.hs @@ -8,7 +8,7 @@ module Stackctl.Commands import Stackctl.Prelude -import Stackctl.Colors +import Stackctl.ColorOption import Stackctl.DirectoryOption import Stackctl.FilterOption import Stackctl.Spec.Capture diff --git a/src/Stackctl/Options.hs b/src/Stackctl/Options.hs index fcd82be..26d6675 100644 --- a/src/Stackctl/Options.hs +++ b/src/Stackctl/Options.hs @@ -29,9 +29,6 @@ directoryL = lens oDirectory $ \x y -> x { oDirectory = y } filterL :: Lens' Options (Maybe FilterOption) filterL = lens oFilter $ \x y -> x { oFilter = y } -colorL :: Lens' Options (Maybe ColorOption) -colorL = lens oColor $ \x y -> x { oColor = y } - instance HasDirectoryOption Options where directoryOptionL = directoryL . maybeLens defaultDirectoryOption @@ -39,7 +36,7 @@ instance HasFilterOption Options where filterOptionL = filterL . maybeLens defaultFilterOption instance HasColorOption Options where - colorOptionL = colorL . maybeLens defaultColorOption + colorOptionL = lens oColor $ \x y -> x { oColor = y } instance HasVerboseOption Options where verboseOptionL = lens oVerbose $ \x y -> x { oVerbose = y } @@ -59,5 +56,5 @@ optionsParser :: Parser Options optionsParser = Options <$> optional directoryOption <*> optional (filterOption "specifications") - <*> (Just <$> colorOption) + <*> optional colorOption <*> verboseOption diff --git a/src/Stackctl/Spec/Cat.hs b/src/Stackctl/Spec/Cat.hs index c72c4c2..4a1235d 100644 --- a/src/Stackctl/Spec/Cat.hs +++ b/src/Stackctl/Spec/Cat.hs @@ -62,7 +62,6 @@ runCat , HasConfig env , HasDirectoryOption env , HasFilterOption env - , HasColorOption env ) => CatOptions -> m () From b04e1790dc523cea39e07c868b4fa328f4e453cb Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 17 Jan 2023 08:54:35 -0500 Subject: [PATCH 042/187] Version bump --- CHANGELOG.md | 8 +++++++- package.yaml | 2 +- stackctl.cabal | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87e43f0..a1551de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,10 @@ -## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.3.0.0...main) +## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.3.0.1...main) + +## [v1.3.0.1](https://github.com/freckle/stackctl/compare/v1.3.0.0...v1.3.0.1) + +- Fix bug where `LOG_COLOR` was never respected +- Also accept `"required_version: == "` +- Add `Eq`, `ToJSON` instance on `RequiredVersion` ## [v1.3.0.0](https://github.com/freckle/stackctl/compare/v1.2.0.1...v1.3.0.0) diff --git a/package.yaml b/package.yaml index d0b8afd..fe66ee4 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: stackctl -version: 1.3.0.0 +version: 1.3.0.1 github: freckle/stackctl license: MIT author: Freckle Engineering diff --git a/stackctl.cabal b/stackctl.cabal index 717d785..2a0777d 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -5,7 +5,7 @@ cabal-version: 1.18 -- see: https://github.com/sol/hpack name: stackctl -version: 1.3.0.0 +version: 1.3.0.1 description: Please see homepage: https://github.com/freckle/stackctl#readme bug-reports: https://github.com/freckle/stackctl/issues From e790cc37fcb01022a1a661acd3889f26f4d23c84 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 24 Jan 2023 10:18:55 -0500 Subject: [PATCH 043/187] Output "Deleting Stack" immediately Otherwise, things can appear hung. --- src/Stackctl/Spec/Deploy.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Stackctl/Spec/Deploy.hs b/src/Stackctl/Spec/Deploy.hs index 4857e4a..400a86f 100644 --- a/src/Stackctl/Spec/Deploy.hs +++ b/src/Stackctl/Spec/Deploy.hs @@ -127,6 +127,7 @@ handleRollbackComplete confirmation stackName = do logError "Refusing to delete without confirmation" exitFailure + logInfo $ "Deleting Stack" :# ["stackName" .= stackName] result <- awsCloudFormationDeleteStack stackName case result of From 013515e4caff6b28c5c7d4b5aef8f928500a0077 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 24 Jan 2023 10:35:18 -0500 Subject: [PATCH 044/187] Remove extra message metadata The calling function already adds the `stackName` via `withThreadContext`, so this was redundant. --- src/Stackctl/Spec/Deploy.hs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Stackctl/Spec/Deploy.hs b/src/Stackctl/Spec/Deploy.hs index 400a86f..60e406d 100644 --- a/src/Stackctl/Spec/Deploy.hs +++ b/src/Stackctl/Spec/Deploy.hs @@ -118,8 +118,7 @@ handleRollbackComplete confirmation stackName = do when (maybe False stackIsRollbackComplete mStack) $ do logWarn - $ "Stack is in ROLLBACK_COMPLETE state and must be deleted before proceeding" - :# ["stackName" .= stackName] + "Stack is in ROLLBACK_COMPLETE state and must be deleted before proceeding" case confirmation of DeployWithConfirmation -> promptContinue @@ -127,7 +126,7 @@ handleRollbackComplete confirmation stackName = do logError "Refusing to delete without confirmation" exitFailure - logInfo $ "Deleting Stack" :# ["stackName" .= stackName] + logInfo "Deleting Stack" result <- awsCloudFormationDeleteStack stackName case result of From 0aa385b7156ce07df5d3339f78176083a306e303 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 24 Jan 2023 11:00:25 -0500 Subject: [PATCH 045/187] Offer to delete ROLLBACK_FAILED Stacks too There's no reason not to _try_ the auto-delete for this status too, but we do show another warning about permissions, since `ROLLBACK_FAILED` usually indicates the user didn't have enough permissions to remove so-far-created resources -- and they're likely to be that same user now, and still lack those permissions. --- src/Stackctl/AWS/CloudFormation.hs | 14 ++++++++++---- src/Stackctl/Spec/Deploy.hs | 15 +++++++++------ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/Stackctl/AWS/CloudFormation.hs b/src/Stackctl/AWS/CloudFormation.hs index 29dbc5f..aa5d9e3 100644 --- a/src/Stackctl/AWS/CloudFormation.hs +++ b/src/Stackctl/AWS/CloudFormation.hs @@ -2,7 +2,8 @@ module Stackctl.AWS.CloudFormation ( Stack(..) , stack_stackName , stackDescription - , stackIsRollbackComplete + , stackStatusRequiresDeletion + , StackStatus(..) , StackId(..) , StackName(..) , StackDescription(..) @@ -479,9 +480,14 @@ stackIsAbandonedCreate stack = stack ^. stack_stackStatus == StackStatus_REVIEW_IN_PROGRESS && isNothing (stack ^. stack_lastUpdatedTime) -stackIsRollbackComplete :: Stack -> Bool -stackIsRollbackComplete stack = - stack ^. stack_stackStatus == StackStatus_ROLLBACK_COMPLETE +stackStatusRequiresDeletion :: Stack -> Maybe StackStatus +stackStatusRequiresDeletion stack = status + <$ guard (status `elem` requiresDeletionStatuses) + where status = stack ^. stack_stackStatus + +requiresDeletionStatuses :: [StackStatus] +requiresDeletionStatuses = + [StackStatus_ROLLBACK_COMPLETE, StackStatus_ROLLBACK_FAILED] runningStatuses :: [StackStatus] runningStatuses = diff --git a/src/Stackctl/Spec/Deploy.hs b/src/Stackctl/Spec/Deploy.hs index 60e406d..2387682 100644 --- a/src/Stackctl/Spec/Deploy.hs +++ b/src/Stackctl/Spec/Deploy.hs @@ -75,7 +75,8 @@ runDeploy DeployOptions {..} = do for_ specs $ \spec -> do withThreadContext ["stackName" .= stackSpecStackName spec] $ do - handleRollbackComplete sdoDeployConfirmation $ stackSpecStackName spec + checkIfStackRequiresDeletion sdoDeployConfirmation + $ stackSpecStackName spec emChangeSet <- createChangeSet spec sdoParameters sdoTags @@ -102,7 +103,7 @@ data DeployConfirmation | DeployWithoutConfirmation deriving stock Eq -handleRollbackComplete +checkIfStackRequiresDeletion :: ( MonadUnliftIO m , MonadResource m , MonadLogger m @@ -113,12 +114,14 @@ handleRollbackComplete => DeployConfirmation -> StackName -> m () -handleRollbackComplete confirmation stackName = do +checkIfStackRequiresDeletion confirmation stackName = do mStack <- awsCloudFormationDescribeStackMaybe stackName - when (maybe False stackIsRollbackComplete mStack) $ do - logWarn - "Stack is in ROLLBACK_COMPLETE state and must be deleted before proceeding" + for_ (stackStatusRequiresDeletion =<< mStack) $ \status -> do + logWarn $ "Stack must be deleted before proceeding" :# ["status" .= status] + when (status == StackStatus_ROLLBACK_FAILED) + $ logWarn + "Stack is in ROLLBACK_FAILED. This may require elevated permissions for the delete to succeed" case confirmation of DeployWithConfirmation -> promptContinue From 384066b09335e3750abbbf679c5175a570dbf16f Mon Sep 17 00:00:00 2001 From: Pat Brisbin Date: Tue, 24 Jan 2023 11:11:39 -0500 Subject: [PATCH 046/187] Update src/Stackctl/AWS/CloudFormation.hs --- src/Stackctl/AWS/CloudFormation.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Stackctl/AWS/CloudFormation.hs b/src/Stackctl/AWS/CloudFormation.hs index aa5d9e3..71d7094 100644 --- a/src/Stackctl/AWS/CloudFormation.hs +++ b/src/Stackctl/AWS/CloudFormation.hs @@ -3,10 +3,10 @@ module Stackctl.AWS.CloudFormation , stack_stackName , stackDescription , stackStatusRequiresDeletion - , StackStatus(..) , StackId(..) , StackName(..) , StackDescription(..) + , StackStatus(..) , StackEvent(..) , ResourceStatus(..) , stackEvent_eventId From 5efb28c702422abf3d3d2698e1143468d6f5e348 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 24 Jan 2023 11:27:06 -0500 Subject: [PATCH 047/187] Add --no-include-full option to stackctl-changes The purpose of this command is primarily to build a PR comment to post to PRs that change resources. If the comment is no different than the last one (e.g. you make a fixup in an existing PR), our add-pr-comment action could easily skip adding a duplicate comment. Problem is, the full JSON contains identifiers that are different every time, causing the comment body to never be an actual duplicate. I've never found these details particularly valuable, so let's have an option to omit them, making it a little easier for the duplicate-comment logic to work for us. --- doc/stackctl-changes.1.md | 5 +++ src/Stackctl/Spec/Changes.hs | 5 ++- src/Stackctl/Spec/Changes/Format.hs | 54 +++++++++++++++++++---------- 3 files changed, 45 insertions(+), 19 deletions(-) diff --git a/doc/stackctl-changes.1.md b/doc/stackctl-changes.1.md index bac3d00..cd9bd9a 100644 --- a/doc/stackctl-changes.1.md +++ b/doc/stackctl-changes.1.md @@ -22,6 +22,11 @@ successful operation. > Output changes in **FORMAT**. See dedicated section. +**\--no-include-full**\ + +> Don't include full ChangeSet JSON details. This option only applies to the +> *pr* format. + **\-p**, **\--parameter** *\*\ > Override the given Parameter for this operation. Omitting *VALUE* will result diff --git a/src/Stackctl/Spec/Changes.hs b/src/Stackctl/Spec/Changes.hs index 612adb4..7ccc279 100644 --- a/src/Stackctl/Spec/Changes.hs +++ b/src/Stackctl/Spec/Changes.hs @@ -24,6 +24,7 @@ import Stackctl.TagOption data ChangesOptions = ChangesOptions { scoFormat :: Format + , scoOmitFull :: OmitFull , scoParameters :: [Parameter] , scoTags :: [Tag] , scoOutput :: Maybe FilePath @@ -34,6 +35,7 @@ data ChangesOptions = ChangesOptions parseChangesOptions :: Parser ChangesOptions parseChangesOptions = ChangesOptions <$> formatOption + <*> omitFullOption <*> many parameterOption <*> many tagOption <*> optional (argument str @@ -78,7 +80,8 @@ runChanges ChangesOptions {..} = do let name = pack $ stackSpecPathFilePath $ stackSpecSpecPath spec - formatted = formatChangeSet colors name scoFormat mChangeSet + formatted = + formatChangeSet colors scoOmitFull name scoFormat mChangeSet case scoOutput of Nothing -> pushLoggerLn formatted diff --git a/src/Stackctl/Spec/Changes/Format.hs b/src/Stackctl/Spec/Changes/Format.hs index 9f09135..f7b5279 100644 --- a/src/Stackctl/Spec/Changes/Format.hs +++ b/src/Stackctl/Spec/Changes/Format.hs @@ -1,6 +1,8 @@ module Stackctl.Spec.Changes.Format ( Format(..) , formatOption + , OmitFull(..) + , omitFullOption , formatChangeSet , formatTTY ) where @@ -17,6 +19,10 @@ data Format = FormatTTY | FormatPullRequest +data OmitFull + = OmitFull + | IncludeFull + formatOption :: Parser Format formatOption = option (eitherReader readFormat) $ mconcat [ short 'f' @@ -37,10 +43,19 @@ showFormat = \case FormatTTY -> "tty" FormatPullRequest -> "pr" -formatChangeSet :: Colors -> Text -> Format -> Maybe ChangeSet -> Text -formatChangeSet colors name = \case +-- brittany-disable-next-binding + +omitFullOption :: Parser OmitFull +omitFullOption = flag IncludeFull OmitFull + ( long "no-include-full" + <> help "Don't include full ChangeSet JSON details" + ) + +formatChangeSet + :: Colors -> OmitFull -> Text -> Format -> Maybe ChangeSet -> Text +formatChangeSet colors omitFull name = \case FormatTTY -> formatTTY colors name - FormatPullRequest -> formatPullRequest name + FormatPullRequest -> formatPullRequest omitFull name formatTTY :: Colors -> Text -> Maybe ChangeSet -> Text formatTTY colors@Colors {..} name mChangeSet = case (mChangeSet, rChanges) of @@ -83,15 +98,15 @@ formatTTY colors@Colors {..} name mChangeSet = case (mChangeSet, rChanges) of x@Replacement_Conditional -> yellow (toText x) Replacement' x -> x -formatPullRequest :: Text -> Maybe ChangeSet -> Text -formatPullRequest name mChangeSet = +formatPullRequest :: OmitFull -> Text -> Maybe ChangeSet -> Text +formatPullRequest omitFull name mChangeSet = emoji <> " This PR generates " <> description <> " for `" <> name <> "`." - <> fromMaybe "" (commentBody <$> mChangeSet <*> rChanges) + <> fromMaybe "" (commentBody omitFull <$> mChangeSet <*> rChanges) <> "\n" where emoji = case (mChangeSet, nChanges) of @@ -112,24 +127,27 @@ formatPullRequest name mChangeSet = changes <- csChanges cs NE.nonEmpty $ mapMaybe resourceChange changes -commentBody :: ChangeSet -> NonEmpty ResourceChange -> Text -commentBody cs rcs = +commentBody :: OmitFull -> ChangeSet -> NonEmpty ResourceChange -> Text +commentBody omitFull cs rcs = mconcat $ [ "\n" , "\n| Action | Logical Id | Physical Id | Type | Replacement | Scope | Details |" , "\n| --- | --- | --- | --- | --- | --- | --- |" ] <> map commentTableRow (NE.toList rcs) - <> [ "\n" - , "\n
" - , "\nFull changes" - , "\n" - , "\n```json" - , "\n" <> changeSetJSON cs - , "\n```" - , "\n" - , "\n
" - ] + <> case omitFull of + OmitFull -> [] + IncludeFull -> + [ "\n" + , "\n
" + , "\nFull changes" + , "\n" + , "\n```json" + , "\n" <> changeSetJSON cs + , "\n```" + , "\n" + , "\n
" + ] commentTableRow :: ResourceChange -> Text commentTableRow ResourceChange' {..} = mconcat From adc6e67543aad3613bb81e5007232dc805214435 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 31 Jan 2023 11:44:07 -0500 Subject: [PATCH 048/187] Ignore optim changes --- package.yaml | 1 + stackctl.cabal | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/package.yaml b/package.yaml index fe66ee4..0337210 100644 --- a/package.yaml +++ b/package.yaml @@ -16,6 +16,7 @@ dependencies: - base >= 4 && < 5 ghc-options: + - -fignore-optim-changes - -fwrite-ide-info - -Weverything - -Wno-all-missed-specialisations diff --git a/stackctl.cabal b/stackctl.cabal index 2a0777d..a836726 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -93,7 +93,7 @@ library StandaloneDeriving TypeApplications TypeFamilies - ghc-options: -fwrite-ide-info -Weverything -Wno-all-missed-specialisations -Wno-missing-import-lists -Wno-missing-kind-signatures -Wno-missing-local-signatures -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module -Wno-unsafe -optP-Wno-nonportable-include-path + ghc-options: -fignore-optim-changes -fwrite-ide-info -Weverything -Wno-all-missed-specialisations -Wno-missing-import-lists -Wno-missing-kind-signatures -Wno-missing-local-signatures -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module -Wno-unsafe -optP-Wno-nonportable-include-path build-depends: Blammo >=1.1.1.1 , Glob @@ -166,7 +166,7 @@ executable stackctl StandaloneDeriving TypeApplications TypeFamilies - ghc-options: -fwrite-ide-info -Weverything -Wno-all-missed-specialisations -Wno-missing-import-lists -Wno-missing-kind-signatures -Wno-missing-local-signatures -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module -Wno-unsafe -optP-Wno-nonportable-include-path -threaded -rtsopts -with-rtsopts=-N + ghc-options: -fignore-optim-changes -fwrite-ide-info -Weverything -Wno-all-missed-specialisations -Wno-missing-import-lists -Wno-missing-kind-signatures -Wno-missing-local-signatures -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module -Wno-unsafe -optP-Wno-nonportable-include-path -threaded -rtsopts -with-rtsopts=-N build-depends: base ==4.* , stackctl @@ -212,7 +212,7 @@ test-suite spec StandaloneDeriving TypeApplications TypeFamilies - ghc-options: -fwrite-ide-info -Weverything -Wno-all-missed-specialisations -Wno-missing-import-lists -Wno-missing-kind-signatures -Wno-missing-local-signatures -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module -Wno-unsafe -optP-Wno-nonportable-include-path + ghc-options: -fignore-optim-changes -fwrite-ide-info -Weverything -Wno-all-missed-specialisations -Wno-missing-import-lists -Wno-missing-kind-signatures -Wno-missing-local-signatures -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module -Wno-unsafe -optP-Wno-nonportable-include-path build-depends: QuickCheck , aeson From 270e1108f65b66cb7ba4afcca2fcb3d53792d590 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 31 Jan 2023 11:44:41 -0500 Subject: [PATCH 049/187] Extend Lambda invoke timeout The maximum timeout on functions themselves is 15 minutes. Therefore, we need to allow up to that (plus some buffer) when invoking them. --- src/Stackctl/AWS/Core.hs | 6 +++++- src/Stackctl/AWS/Lambda.hs | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Stackctl/AWS/Core.hs b/src/Stackctl/AWS/Core.hs index e80282c..90c766d 100644 --- a/src/Stackctl/AWS/Core.hs +++ b/src/Stackctl/AWS/Core.hs @@ -9,6 +9,7 @@ module Stackctl.AWS.Core -- * Modifiers on 'AwsEnv' , awsWithin + , awsTimeout -- * 'Amazonka' extensions , AccountId(..) @@ -20,7 +21,7 @@ module Stackctl.AWS.Core , MonadResource ) where -import Stackctl.Prelude +import Stackctl.Prelude hiding (timeout) import Amazonka hiding (LogLevel(..)) import qualified Amazonka as AWS @@ -106,6 +107,9 @@ awsAwait w req = do awsWithin :: (MonadReader env m, HasAwsEnv env) => Region -> m a -> m a awsWithin r = local $ over (awsEnvL . unL) (within r) +awsTimeout :: (MonadReader env m, HasAwsEnv env) => Seconds -> m a -> m a +awsTimeout t = local $ over (awsEnvL . unL) (timeout t) + newtype AccountId = AccountId { unAccountId :: Text } diff --git a/src/Stackctl/AWS/Lambda.hs b/src/Stackctl/AWS/Lambda.hs index 7747db0..a5566b8 100644 --- a/src/Stackctl/AWS/Lambda.hs +++ b/src/Stackctl/AWS/Lambda.hs @@ -71,7 +71,9 @@ awsLambdaInvoke awsLambdaInvoke name payload = do logDebug $ "Invoking function" :# ["name" .= name] - resp <- awsSend $ newInvoke name $ BSL.toStrict $ encode payload + -- Match Lambda's own limit (15 minutes) and add some buffer + resp <- awsTimeout 905 $ awsSend $ newInvoke name $ BSL.toStrict $ encode + payload let status = resp ^. invokeResponse_statusCode From fc2b1ef7650e33e20c94897645a341fb44e00a9c Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 31 Jan 2023 11:47:59 -0500 Subject: [PATCH 050/187] Version bump --- CHANGELOG.md | 7 ++++++- package.yaml | 2 +- stackctl.cabal | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1551de..b909e45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ -## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.3.0.1...main) +## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.3.0.2...main) + +## [v1.3.0.2](https://github.com/freckle/stackctl/compare/v1.3.0.1...v1.3.0.2) + +- Adjust timeout when invoking Lambdas to allow up to Lambda's own execution + timeout (15 minutes). ## [v1.3.0.1](https://github.com/freckle/stackctl/compare/v1.3.0.0...v1.3.0.1) diff --git a/package.yaml b/package.yaml index 0337210..0f36810 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: stackctl -version: 1.3.0.1 +version: 1.3.0.2 github: freckle/stackctl license: MIT author: Freckle Engineering diff --git a/stackctl.cabal b/stackctl.cabal index a836726..fbbe77e 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -5,7 +5,7 @@ cabal-version: 1.18 -- see: https://github.com/sol/hpack name: stackctl -version: 1.3.0.1 +version: 1.3.0.2 description: Please see homepage: https://github.com/freckle/stackctl#readme bug-reports: https://github.com/freckle/stackctl/issues From 6064e834c36cfc887a16a85ef84d506cf0f652a2 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Fri, 3 Feb 2023 09:37:33 -0500 Subject: [PATCH 051/187] Refactor Generate, and make writeStackSpec idempotent Implementing a new `platform-provision` (name TBD) subcommand created a new use-case for `Generate` and tripped over some latent warts that resulted from its organic growth: 1. The 4 `g{Stack,Template}*` fields interact in complicated ways. Using them in a use-case where we always want to use a pre-existing template was not intuitive and introduced overwrite risk that could not be cleanly resolved 2. `writeStackSpec` was not idempotent. Introducing an exists check was trivial; introducing an `overwrite` back-door (required for `platform-deploy`) was also relatively easy, but it was not easy to handle potential overwrite of the specs vs the template, mostly due to problem (1) 3. Even though you're likely already in a reader with `HasDirectoryOption`, you're still required to pass an explicit output directory This commit fixes these, to unblock `platform-provision` (or whatever it'll be called). --- src/Stackctl/Spec/Capture.hs | 23 +++++++------ src/Stackctl/Spec/Generate.hs | 61 ++++++++++++++++++++++------------- src/Stackctl/StackSpec.hs | 30 +++++++++++++---- 3 files changed, 75 insertions(+), 39 deletions(-) diff --git a/src/Stackctl/Spec/Capture.hs b/src/Stackctl/Spec/Capture.hs index b528fe7..557d103 100644 --- a/src/Stackctl/Spec/Capture.hs +++ b/src/Stackctl/Spec/Capture.hs @@ -10,7 +10,7 @@ import Options.Applicative import Stackctl.AWS import Stackctl.AWS.Scope import Stackctl.Config (HasConfig) -import Stackctl.DirectoryOption (HasDirectoryOption(..), unDirectoryOption) +import Stackctl.DirectoryOption (HasDirectoryOption) import Stackctl.Spec.Generate import Stackctl.StackSpec import System.FilePath.Glob @@ -74,26 +74,29 @@ runCapture => CaptureOptions -> m () runCapture CaptureOptions {..} = do - dir <- unDirectoryOption <$> view directoryOptionL - let setScopeName scope = maybe scope (\name -> scope { awsAccountName = name }) scoAccountName generate' stack template path templatePath = do + let + stackName = StackName $ stack ^. stack_stackName + templateBody = templateBodyFromValue template + void $ local (awsScopeL %~ setScopeName) $ generate Generate - { gOutputDirectory = dir - , gTemplatePath = templatePath - , gTemplateFormat = scoTemplateFormat - , gStackPath = path - , gStackName = StackName $ stack ^. stack_stackName - , gDescription = stackDescription stack + { gDescription = stackDescription stack , gDepends = scoDepends , gActions = Nothing , gParameters = parameters stack , gCapabilities = capabilities stack , gTags = tags stack - , gTemplateBody = templateBodyFromValue template + , gSpec = case path of + Nothing -> GenerateSpec stackName + Just sp -> GenerateSpecTo stackName sp + , gTemplate = case templatePath of + Nothing -> GenerateTemplate templateBody scoTemplateFormat + Just tp -> GenerateTemplateTo templateBody tp + , gOverwrite = False } results <- awsCloudFormationGetStackNamesMatching scoStackName diff --git a/src/Stackctl/Spec/Generate.hs b/src/Stackctl/Spec/Generate.hs index 09e5563..c316954 100644 --- a/src/Stackctl/Spec/Generate.hs +++ b/src/Stackctl/Spec/Generate.hs @@ -1,5 +1,7 @@ module Stackctl.Spec.Generate ( Generate(..) + , GenerateSpec(..) + , GenerateTemplate(..) , generate , TemplateFormat(..) ) where @@ -10,29 +12,38 @@ import Stackctl.Action import Stackctl.AWS import Stackctl.AWS.Scope import Stackctl.Config (HasConfig) +import Stackctl.DirectoryOption import Stackctl.Spec.Discover (buildSpecPath) import Stackctl.StackSpec import Stackctl.StackSpecPath import Stackctl.StackSpecYaml data Generate = Generate - { gOutputDirectory :: FilePath - , gTemplatePath :: Maybe FilePath - -- ^ If not given, will use @{stack-name}.(yaml|json)@ - , gTemplateFormat :: TemplateFormat - -- ^ Ignored if 'gTemplatePath' is given - , gStackPath :: Maybe FilePath - -- ^ If not given, will use @{stack-name}.yaml@ - , gStackName :: StackName - , gDescription :: Maybe StackDescription + { gDescription :: Maybe StackDescription , gDepends :: Maybe [StackName] , gActions :: Maybe [Action] , gParameters :: Maybe [Parameter] , gCapabilities :: Maybe [Capability] , gTags :: Maybe [Tag] - , gTemplateBody :: TemplateBody + , gSpec :: GenerateSpec + , gTemplate :: GenerateTemplate + , gOverwrite :: Bool } +data GenerateSpec + = GenerateSpec StackName + -- ^ Generate at an inferred name + | GenerateSpecTo StackName FilePath + -- ^ Generate to a given path + +data GenerateTemplate + = GenerateTemplate TemplateBody TemplateFormat + -- ^ Generate at an inferred name + | GenerateTemplateTo TemplateBody FilePath + -- ^ Generate to the given path + | UseExistingTemplate FilePath + -- ^ Assume template exists + data TemplateFormat = TemplateFormatYaml | TemplateFormatJson @@ -44,23 +55,26 @@ generate , MonadReader env m , HasConfig env , HasAwsScope env + , HasDirectoryOption env ) => Generate -> m FilePath generate Generate {..} = do let - defaultStackPath = unpack (unStackName gStackName) <.> "yaml" - defaultTemplatePath = - unpack (unStackName gStackName) <.> case gTemplateFormat of - TemplateFormatYaml -> "yaml" - TemplateFormatJson -> "json" + (stackName, stackPath) = case gSpec of + GenerateSpec name -> (name, unpack (unStackName name) <> ".yaml") + GenerateSpecTo name path -> (name, path) - stackPath = fromMaybe defaultStackPath gStackPath + (mTemplateBody, templatePath) = case gTemplate of + GenerateTemplate body format -> + ( Just body + , case format of + TemplateFormatYaml -> unpack (unStackName stackName) <> ".yaml" + TemplateFormatJson -> unpack (unStackName stackName) <> ".json" + ) + GenerateTemplateTo body path -> (Just body, path) + UseExistingTemplate path -> (Nothing, path) - specPath <- buildSpecPath gStackName stackPath - - let - templatePath = fromMaybe defaultTemplatePath gTemplatePath specYaml = StackSpecYaml { ssyDescription = gDescription , ssyTemplate = templatePath @@ -71,9 +85,10 @@ generate Generate {..} = do , ssyTags = tagsYaml . map TagYaml <$> gTags } - stackSpec <- buildStackSpec gOutputDirectory specPath specYaml + dir <- view $ directoryOptionL . to unDirectoryOption + specPath <- buildSpecPath stackName stackPath + stackSpec <- buildStackSpec dir specPath specYaml withThreadContext ["stackName" .= stackSpecStackName stackSpec] $ do - logInfo "Generating specification" - writeStackSpec stackSpec gTemplateBody + writeStackSpec gOverwrite stackSpec mTemplateBody pure $ stackSpecPathFilePath specPath diff --git a/src/Stackctl/StackSpec.hs b/src/Stackctl/StackSpec.hs index f118107..3de9acd 100644 --- a/src/Stackctl/StackSpec.hs +++ b/src/Stackctl/StackSpec.hs @@ -34,7 +34,7 @@ import Stackctl.StackSpecPath import Stackctl.StackSpecYaml import qualified System.FilePath as FilePath import System.FilePath (takeExtension) -import UnliftIO.Directory (createDirectoryIfMissing) +import UnliftIO.Directory (createDirectoryIfMissing, doesFileExist) data StackSpec = StackSpec { ssSpecRoot :: FilePath @@ -135,11 +135,29 @@ writeTemplateBody path body = do dir = takeDirectory path ext = takeExtension path -writeStackSpec :: MonadUnliftIO m => StackSpec -> TemplateBody -> m () -writeStackSpec stackSpec templateBody = do - writeTemplateBody templatePath templateBody - createDirectoryIfMissing True $ takeDirectory specPath - liftIO $ Yaml.encodeFile specPath $ stackSpecSpecBody stackSpec +writeStackSpec + :: (MonadUnliftIO m, MonadLogger m) + => Bool + -> StackSpec + -> Maybe TemplateBody + -> m () +writeStackSpec overwrite stackSpec mTemplateBody = do + for_ mTemplateBody $ \templateBody -> do + logInfo $ "Writing template" :# ["path" .= templatePath] + writeTemplateBody templatePath templateBody + + exists <- doesFileExist specPath + + if exists && not overwrite + then do + let + reason :: Text + reason = "file exists and overwrite not set" + logInfo $ "Skipping" :# ["path" .= specPath, "reason" .= reason] + else do + logInfo $ "Writing specification" :# ["path" .= specPath] + createDirectoryIfMissing True $ takeDirectory specPath + liftIO $ Yaml.encodeFile specPath $ stackSpecSpecBody stackSpec where templatePath = unStackTemplate $ stackSpecTemplate stackSpec specPath = From 7330be35e9d0e29fca358b812bb0556a16dd70e9 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Fri, 3 Feb 2023 12:43:56 -0500 Subject: [PATCH 052/187] Add awsAssumeRole Pretty much does what it says on the tin. --- src/Stackctl/AWS/Core.hs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/Stackctl/AWS/Core.hs b/src/Stackctl/AWS/Core.hs index 90c766d..8466723 100644 --- a/src/Stackctl/AWS/Core.hs +++ b/src/Stackctl/AWS/Core.hs @@ -6,6 +6,7 @@ module Stackctl.AWS.Core , awsSend , awsPaginate , awsAwait + , awsAssumeRole -- * Modifiers on 'AwsEnv' , awsWithin @@ -25,6 +26,8 @@ import Stackctl.Prelude hiding (timeout) import Amazonka hiding (LogLevel(..)) import qualified Amazonka as AWS +import Amazonka.Auth.Keys (fromSession) +import Amazonka.STS.AssumeRole import Conduit (ConduitM) import Control.Monad.Logger (defaultLoc, toLogStr) import Control.Monad.Trans.Resource (MonadResource) @@ -104,6 +107,27 @@ awsAwait w req = do AwsEnv env <- view awsEnvL await env w req +awsAssumeRole + :: (MonadResource m, MonadReader env m, HasAwsEnv env) + => Text + -> Text + -> m a + -> m a +awsAssumeRole role sessionName f = do + let req = newAssumeRole role sessionName + + assumeEnv <- awsSimple "sts:AssumeRole" req $ \resp -> do + creds <- resp ^. assumeRoleResponse_credentials + token <- creds ^. authSessionToken + + let + accessKeyId = creds ^. authAccessKeyId + secretAccessKey = creds ^. authSecretAccessKey + + pure $ fromSession accessKeyId secretAccessKey token + + local (awsEnvL . unL %~ assumeEnv) f + awsWithin :: (MonadReader env m, HasAwsEnv env) => Region -> m a -> m a awsWithin r = local $ over (awsEnvL . unL) (within r) From 69110e1da39d7ce1734a87aa9f585363adf1af54 Mon Sep 17 00:00:00 2001 From: Pat Brisbin Date: Mon, 6 Feb 2023 09:17:07 -0500 Subject: [PATCH 053/187] Update src/Stackctl/AWS/Core.hs --- src/Stackctl/AWS/Core.hs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Stackctl/AWS/Core.hs b/src/Stackctl/AWS/Core.hs index 8466723..de5bff5 100644 --- a/src/Stackctl/AWS/Core.hs +++ b/src/Stackctl/AWS/Core.hs @@ -110,8 +110,11 @@ awsAwait w req = do awsAssumeRole :: (MonadResource m, MonadReader env m, HasAwsEnv env) => Text + -- ^ Role ARN -> Text + -- ^ Session name -> m a + -- ^ Action to run as the assumed role -> m a awsAssumeRole role sessionName f = do let req = newAssumeRole role sessionName From b6673a6fcfefb1be5a034e23a74c4c88c0fb8eeb Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Mon, 6 Feb 2023 13:40:36 -0500 Subject: [PATCH 054/187] Version bump --- CHANGELOG.md | 8 +++++++- package.yaml | 2 +- stackctl.cabal | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b909e45..2c656c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,10 @@ -## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.3.0.2...main) +## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.4.0.0...main) + +## [v1.4.0.0](https://github.com/freckle/stackctl/compare/v1.3.0.2...v1.4.0.0) + +- Add `awsAssumeRole` for running an action as an assumed role +- Refactor `Generate` interface to better support generating stacks with + pre-existing templates ## [v1.3.0.2](https://github.com/freckle/stackctl/compare/v1.3.0.1...v1.3.0.2) diff --git a/package.yaml b/package.yaml index 0f36810..71c17e2 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: stackctl -version: 1.3.0.2 +version: 1.4.0.0 github: freckle/stackctl license: MIT author: Freckle Engineering diff --git a/stackctl.cabal b/stackctl.cabal index fbbe77e..f95cc34 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -5,7 +5,7 @@ cabal-version: 1.18 -- see: https://github.com/sol/hpack name: stackctl -version: 1.3.0.2 +version: 1.4.0.0 description: Please see homepage: https://github.com/freckle/stackctl#readme bug-reports: https://github.com/freckle/stackctl/issues From fdb99c911dd118dee4a46358da98ceedcebc2e09 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 8 Feb 2023 14:26:30 -0500 Subject: [PATCH 055/187] Delete corresponding Stacks when specs are removed Given: - A full path to `stacks/.../foo/bar.yaml` is included in `--filter` - And that path does not exist - But the corresponding, conventionally-named Stack `foo-bar` does Then `stackctl` will handle this as a delete, meaning it'll be shown as such in `changes` and processed in `deploy`. This is difficult to trigger as a manual operator, which is fine. The idea is that operators will remove files and open a PR. Since the removed file will be in the PR changed-files, and so included in `--filter`, the above will apply and required removals will be processed like any other gitops change. --- package.yaml | 1 + src/Stackctl/AWS/Scope.hs | 41 +++++++++++++++++++++ src/Stackctl/FilterOption.hs | 4 +++ src/Stackctl/RemovedStack.hs | 43 ++++++++++++++++++++++ src/Stackctl/Spec/Changes.hs | 25 +++++++------ src/Stackctl/Spec/Changes/Format.hs | 7 ++++ src/Stackctl/Spec/Deploy.hs | 50 +++++++++++++++++++++++--- src/Stackctl/Spec/Discover.hs | 23 ++---------- stackctl.cabal | 3 ++ test/Stackctl/AWS/ScopeSpec.hs | 56 +++++++++++++++++++++++++++++ 10 files changed, 217 insertions(+), 36 deletions(-) create mode 100644 src/Stackctl/RemovedStack.hs create mode 100644 test/Stackctl/AWS/ScopeSpec.hs diff --git a/package.yaml b/package.yaml index 71c17e2..54df860 100644 --- a/package.yaml +++ b/package.yaml @@ -89,6 +89,7 @@ library: - semigroups - text - time + - transformers - unliftio - unliftio-core - unordered-containers diff --git a/src/Stackctl/AWS/Scope.hs b/src/Stackctl/AWS/Scope.hs index 49ae049..2ffac7e 100644 --- a/src/Stackctl/AWS/Scope.hs +++ b/src/Stackctl/AWS/Scope.hs @@ -1,13 +1,18 @@ module Stackctl.AWS.Scope ( AwsScope(..) + , awsScopeSpecPatterns + , awsScopeSpecStackName , HasAwsScope(..) , fetchAwsScope ) where import Stackctl.Prelude +import qualified Data.Text as T import Stackctl.AWS import System.Environment (lookupEnv) +import System.FilePath (joinPath, splitPath) +import System.FilePath.Glob (Pattern, compile, match) data AwsScope = AwsScope { awsAccountId :: AccountId @@ -17,6 +22,42 @@ data AwsScope = AwsScope deriving stock (Eq, Show, Generic) deriving anyclass ToJSON +awsScopeSpecPatterns :: AwsScope -> [Pattern] +awsScopeSpecPatterns AwsScope {..} = + [ compile + $ "stacks" + unpack (unAccountId awsAccountId) + <> ".*" + unpack (fromRegion awsRegion) + <> "**" + "*" + <.> "yaml" + , compile + $ "stacks" + "*." + <> unpack (unAccountId awsAccountId) + unpack (fromRegion awsRegion) + <> "**" + "*" + <.> "yaml" + ] + +awsScopeSpecStackName :: AwsScope -> FilePath -> Maybe StackName +awsScopeSpecStackName scope path = do + guard $ any (`match` path) $ awsScopeSpecPatterns scope + + -- once we've guarded that the path matches our scope patterns, we can play it + -- pretty fast and loose with the "parsing" step + pure + $ path -- stacks/account/region/x/y.yaml + & splitPath -- [stacks/, account/, region/, x/, y.yaml] + & drop 3 -- [x, y.yaml] + & joinPath -- x/y.yaml + & dropExtension -- x/y + & pack + & T.replace "/" "-" -- x-y + & StackName + class HasAwsScope env where awsScopeL :: Lens' env AwsScope diff --git a/src/Stackctl/FilterOption.hs b/src/Stackctl/FilterOption.hs index 81c5984..233dedb 100644 --- a/src/Stackctl/FilterOption.hs +++ b/src/Stackctl/FilterOption.hs @@ -6,6 +6,7 @@ module Stackctl.FilterOption , filterOption , filterOptionFromPaths , filterOptionFromText + , filterOptionToPaths , filterStackSpecs ) where @@ -93,6 +94,9 @@ showFilterOption = defaultFilterOption :: FilterOption defaultFilterOption = filterOptionFromPaths $ pure "**/*" +filterOptionToPaths :: FilterOption -> [FilePath] +filterOptionToPaths = map decompile . NE.toList . unFilterOption + filterStackSpecs :: FilterOption -> [StackSpec] -> [StackSpec] filterStackSpecs fo = filter $ \spec -> any (`matchStackSpec` spec) $ unFilterOption fo diff --git a/src/Stackctl/RemovedStack.hs b/src/Stackctl/RemovedStack.hs new file mode 100644 index 0000000..77b2086 --- /dev/null +++ b/src/Stackctl/RemovedStack.hs @@ -0,0 +1,43 @@ +module Stackctl.RemovedStack + ( inferRemovedStacks + ) where + +import Stackctl.Prelude + +import Control.Error.Util (hoistMaybe) +import Control.Monad.Trans.Maybe (MaybeT(..), runMaybeT) +import Stackctl.AWS.CloudFormation +import Stackctl.AWS.Core +import Stackctl.AWS.Scope +import Stackctl.FilterOption +import UnliftIO.Directory (doesFileExist) + +inferRemovedStacks + :: ( MonadUnliftIO m + , MonadResource m + , MonadReader env m + , HasAwsEnv env + , HasAwsScope env + , HasFilterOption env + ) + => m [Stack] +inferRemovedStacks = do + scope <- view awsScopeL + paths <- view $ filterOptionL . to filterOptionToPaths + catMaybes <$> traverse (findRemovedStack scope) paths + +findRemovedStack + :: (MonadUnliftIO m, MonadResource m, MonadReader env m, HasAwsEnv env) + => AwsScope + -> FilePath + -> m (Maybe Stack) +findRemovedStack scope path = runMaybeT $ do + -- The filter is a full path to a specification in the current + -- account/region... + stackName <- hoistMaybe $ awsScopeSpecStackName scope path + + -- that no longer exists... + guard . not =<< doesFileExist path + + -- but the Stack it would point to does + MaybeT $ awsCloudFormationDescribeStackMaybe stackName diff --git a/src/Stackctl/Spec/Changes.hs b/src/Stackctl/Spec/Changes.hs index 7ccc279..19f8ff6 100644 --- a/src/Stackctl/Spec/Changes.hs +++ b/src/Stackctl/Spec/Changes.hs @@ -16,6 +16,7 @@ import Stackctl.Config (HasConfig) import Stackctl.DirectoryOption (HasDirectoryOption) import Stackctl.FilterOption (HasFilterOption) import Stackctl.ParameterOption +import Stackctl.RemovedStack import Stackctl.Spec.Changes.Format import Stackctl.Spec.Discover import Stackctl.StackSpec @@ -63,6 +64,15 @@ runChanges ChangesOptions {..} = do -- Clear file before starting, as we have to use append for each spec liftIO $ traverse_ (`T.writeFile` "") scoOutput + colors <- case scoOutput of + Nothing -> getColorsLogger + Just{} -> pure noColors + + let + write formatted = case scoOutput of + Nothing -> pushLoggerLn formatted + Just p -> liftIO $ T.appendFile p $ formatted <> "\n" + specs <- discoverSpecs for_ specs $ \spec -> do @@ -74,15 +84,8 @@ runChanges ChangesOptions {..} = do logError $ "Error creating ChangeSet" :# ["error" .= err] exitFailure Right mChangeSet -> do - colors <- case scoOutput of - Nothing -> getColorsLogger - Just{} -> pure noColors - - let - name = pack $ stackSpecPathFilePath $ stackSpecSpecPath spec - formatted = - formatChangeSet colors scoOmitFull name scoFormat mChangeSet + let name = pack $ stackSpecPathFilePath $ stackSpecSpecPath spec + write $ formatChangeSet colors scoOmitFull name scoFormat mChangeSet - case scoOutput of - Nothing -> pushLoggerLn formatted - Just p -> liftIO $ T.appendFile p $ formatted <> "\n" + removed <- inferRemovedStacks + traverse_ (write . formatRemovedStack colors scoFormat) removed diff --git a/src/Stackctl/Spec/Changes/Format.hs b/src/Stackctl/Spec/Changes/Format.hs index f7b5279..3447c8a 100644 --- a/src/Stackctl/Spec/Changes/Format.hs +++ b/src/Stackctl/Spec/Changes/Format.hs @@ -4,6 +4,7 @@ module Stackctl.Spec.Changes.Format , OmitFull(..) , omitFullOption , formatChangeSet + , formatRemovedStack , formatTTY ) where @@ -57,6 +58,12 @@ formatChangeSet colors omitFull name = \case FormatTTY -> formatTTY colors name FormatPullRequest -> formatPullRequest omitFull name +formatRemovedStack :: Colors -> Format -> Stack -> Text +formatRemovedStack Colors {..} format stack = case format of + FormatTTY -> red "DELETE" <> " stack " <> cyan name + FormatPullRequest -> ":x: This PR will **delete** the stack `" <> name <> "`" + where name = stack ^. stack_stackName + formatTTY :: Colors -> Text -> Maybe ChangeSet -> Text formatTTY colors@Colors {..} name mChangeSet = case (mChangeSet, rChanges) of (Nothing, _) -> "No changes for " <> name diff --git a/src/Stackctl/Spec/Deploy.hs b/src/Stackctl/Spec/Deploy.hs index 2387682..1a055e7 100644 --- a/src/Stackctl/Spec/Deploy.hs +++ b/src/Stackctl/Spec/Deploy.hs @@ -20,6 +20,7 @@ import Stackctl.DirectoryOption (HasDirectoryOption) import Stackctl.FilterOption (HasFilterOption) import Stackctl.ParameterOption import Stackctl.Prompt +import Stackctl.RemovedStack import Stackctl.Spec.Changes.Format import Stackctl.Spec.Discover import Stackctl.StackSpec @@ -31,6 +32,7 @@ data DeployOptions = DeployOptions , sdoTags :: [Tag] , sdoSaveChangeSets :: Maybe FilePath , sdoDeployConfirmation :: DeployConfirmation + , sdoRemovals :: Bool , sdoClean :: Bool } @@ -50,6 +52,10 @@ parseDeployOptions = DeployOptions ( long "no-confirm" <> help "Don't confirm changes before executing" ) + <*> (not <$> switch + ( long "no-remove" + <> help "Don't delete removed Stacks" + )) <*> switch ( long "clean" <> help "Remove all changesets from Stack after deploy" @@ -98,6 +104,35 @@ runDeploy DeployOptions {..} = do runActions stackName PostDeploy $ stackSpecActions spec when sdoClean $ awsCloudFormationDeleteAllChangeSets stackName + when sdoRemovals $ do + removed <- inferRemovedStacks + traverse_ (deleteRemovedStack sdoDeployConfirmation) removed + +deleteRemovedStack + :: ( MonadMask m + , MonadResource m + , MonadLogger m + , MonadReader env m + , HasLogger env + , HasAwsEnv env + ) + => DeployConfirmation + -> Stack + -> m () +deleteRemovedStack confirmation stack = do + withThreadContext ["stack" .= stackName] $ do + colors <- getColorsLogger + pushLoggerLn $ formatRemovedStack colors FormatTTY stack + + case confirmation of + DeployWithConfirmation -> do + promptContinue + logInfo "Deleting Stack" + DeployWithoutConfirmation -> pure () + + deleteStack stackName + where stackName = StackName $ stack ^. stack_stackName + data DeployConfirmation = DeployWithConfirmation | DeployWithoutConfirmation @@ -130,11 +165,18 @@ checkIfStackRequiresDeletion confirmation stackName = do exitFailure logInfo "Deleting Stack" - result <- awsCloudFormationDeleteStack stackName + deleteStack stackName - case result of - StackDeleteSuccess -> logInfo $ prettyStackDeleteResult result :# [] - StackDeleteFailure{} -> logWarn $ prettyStackDeleteResult result :# [] +deleteStack + :: (MonadResource m, MonadLogger m, MonadReader env m, HasAwsEnv env) + => StackName + -> m () +deleteStack stackName = do + result <- awsCloudFormationDeleteStack stackName + + case result of + StackDeleteSuccess -> logInfo $ prettyStackDeleteResult result :# [] + StackDeleteFailure{} -> logWarn $ prettyStackDeleteResult result :# [] deployChangeSet :: ( MonadUnliftIO m diff --git a/src/Stackctl/Spec/Discover.hs b/src/Stackctl/Spec/Discover.hs index f2e27c9..1793e99 100644 --- a/src/Stackctl/Spec/Discover.hs +++ b/src/Stackctl/Spec/Discover.hs @@ -30,27 +30,8 @@ discoverSpecs => m [StackSpec] discoverSpecs = do dir <- unDirectoryOption <$> view directoryOptionL - scope@AwsScope {..} <- view awsScopeL - paths <- globRelativeTo - dir - [ compile - $ "stacks" - unpack (unAccountId awsAccountId) - <> ".*" - unpack (fromRegion awsRegion) - <> "**" - "*" - <.> "yaml" - , compile - $ "stacks" - "*." - <> unpack (unAccountId awsAccountId) - unpack (fromRegion awsRegion) - <> "**" - "*" - <.> "yaml" - ] - + scope <- view awsScopeL + paths <- globRelativeTo dir $ awsScopeSpecPatterns scope filterOption <- view filterOptionL let diff --git a/stackctl.cabal b/stackctl.cabal index f95cc34..090d54a 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -46,6 +46,7 @@ library Stackctl.ParameterOption Stackctl.Prelude Stackctl.Prompt + Stackctl.RemovedStack Stackctl.Sort Stackctl.Spec.Capture Stackctl.Spec.Cat @@ -127,6 +128,7 @@ library , semigroups , text , time + , transformers , unliftio , unliftio-core , unordered-containers @@ -177,6 +179,7 @@ test-suite spec main-is: Spec.hs other-modules: Stackctl.AWS.CloudFormationSpec + Stackctl.AWS.ScopeSpec Stackctl.Config.RequiredVersionSpec Stackctl.ConfigSpec Stackctl.FilterOptionSpec diff --git a/test/Stackctl/AWS/ScopeSpec.hs b/test/Stackctl/AWS/ScopeSpec.hs new file mode 100644 index 0000000..1388177 --- /dev/null +++ b/test/Stackctl/AWS/ScopeSpec.hs @@ -0,0 +1,56 @@ +module Stackctl.AWS.ScopeSpec + ( spec + ) where + +import Stackctl.Prelude + +import Stackctl.AWS.CloudFormation +import Stackctl.AWS.Core +import Stackctl.AWS.Scope +import Test.Hspec + +spec :: Spec +spec = do + describe "awsScopeSpecStackName" $ do + let + scope = AwsScope + { awsAccountId = AccountId "123" + , awsAccountName = "testing" + , awsRegion = "us-east-1" + } + + it "parses full paths to stacks in the current scope" $ do + awsScopeSpecStackName scope "stacks/123.testing/us-east-1/foo.yaml" + `shouldBe` Just (StackName "foo") + + it "parses name.account style too" $ do + awsScopeSpecStackName scope "stacks/testing.123/us-east-1/foo.yaml" + `shouldBe` Just (StackName "foo") + + it "handles sub-directories" $ do + awsScopeSpecStackName scope "stacks/123.testing/us-east-1/foo/bar.yaml" + `shouldBe` Just (StackName "foo-bar") + + it "handles mismatched name" $ do + awsScopeSpecStackName scope "stacks/123.x/us-east-1/foo.yaml" + `shouldBe` Just (StackName "foo") + + it "handles mismatched name in name.account style" $ do + awsScopeSpecStackName scope "stacks/x.123/us-east-1/foo.yaml" + `shouldBe` Just (StackName "foo") + + it "avoids wrong region" $ do + awsScopeSpecStackName scope "stacks/123.testing/us-east-2/foo.yaml" + `shouldBe` Nothing + + it "avoids arong account id" $ do + awsScopeSpecStackName scope "stacks/124.testing/us-east-1/foo.yaml" + `shouldBe` Nothing + + it "requires a stacks/ prefix" $ do + awsScopeSpecStackName scope "123.testing/us-east-1/foo.yaml" + `shouldBe` Nothing + + it "requires a .yaml suffix" $ do + awsScopeSpecStackName scope "stacks/123.testing/us-east-1/foo.yml" + `shouldBe` Nothing From 3958df3f17126372f32d26a461942b24a38da17f Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 7 Mar 2023 18:04:39 -0500 Subject: [PATCH 056/187] Fix incorrect environment configuration point The option is the singular `--filter`, so the environment variable should be too; `STACKCTL_FILTERS` was a mistake. Since it's relatively easy to do, we'll read it either way for now. --- doc/stackctl.1.md | 4 ++-- src/Stackctl/FilterOption.hs | 14 ++++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/doc/stackctl.1.md b/doc/stackctl.1.md index fa6ea63..185afa6 100644 --- a/doc/stackctl.1.md +++ b/doc/stackctl.1.md @@ -261,9 +261,9 @@ See **stackctl-changes(1)** and **stackctl-deploy(1)**. > Environment-based alternative for *\--directory*. -*STACKCTL_FILTERS*\ +*STACKCTL_FILTER*\ -> Environment-based alternative for *\--filters*. +> Environment-based alternative for *\--filter*. *LOG_\**\ diff --git a/src/Stackctl/FilterOption.hs b/src/Stackctl/FilterOption.hs index 233dedb..bb2f1e3 100644 --- a/src/Stackctl/FilterOption.hs +++ b/src/Stackctl/FilterOption.hs @@ -38,12 +38,14 @@ instance HasFilterOption FilterOption where filterOptionL = id envFilterOption :: String -> Env.Parser Env.Error FilterOption -envFilterOption items = - Env.var (first Env.UnreadError . readFilterOption) "FILTERS" - $ Env.help - $ "Filter " - <> items - <> " by patterns" +envFilterOption items = var "FILTERS" <|> var "FILTER" + where + var name = + Env.var (first Env.UnreadError . readFilterOption) name + $ Env.help + $ "Filter " + <> items + <> " by patterns" filterOption :: String -> Parser FilterOption filterOption items = option (eitherReader readFilterOption) $ mconcat From ecf19c5acd2d2dc738d812cb6317d13e18689899 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 8 Mar 2023 08:50:00 -0500 Subject: [PATCH 057/187] Version bump --- CHANGELOG.md | 7 ++++++- package.yaml | 2 +- stackctl.cabal | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c656c2..eeee9fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ -## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.4.0.0...main) +## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.4.0.1...main) + +## [v1.4.0.1](https://github.com/freckle/stackctl/compare/v1.4.0.0...v1.4.0.1) + +- Document and read a consistently-named `STACKCTL_FILTER` for `--filter`. For + now, the old and incorrect `STACKCTL_FILTERS` will still work. ## [v1.4.0.0](https://github.com/freckle/stackctl/compare/v1.3.0.2...v1.4.0.0) diff --git a/package.yaml b/package.yaml index 54df860..a596a8c 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: stackctl -version: 1.4.0.0 +version: 1.4.0.1 github: freckle/stackctl license: MIT author: Freckle Engineering diff --git a/stackctl.cabal b/stackctl.cabal index 090d54a..02b4239 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -5,7 +5,7 @@ cabal-version: 1.18 -- see: https://github.com/sol/hpack name: stackctl -version: 1.4.0.0 +version: 1.4.0.1 description: Please see homepage: https://github.com/freckle/stackctl#readme bug-reports: https://github.com/freckle/stackctl/issues From 31c58148417e247f0342e824c19dc45181740add Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Fri, 7 Apr 2023 11:04:33 -0400 Subject: [PATCH 058/187] Replace pandoc-based man-page processing with ronn Ronn is a markdown-like format for authoring man-pages. The main benefit to this approach is its nicely-styled, cross-linked html output format, which I'm deployed as a GH Pages site for the project now. The main downside is a slight decrease in quality of installed man-pages, which we consider acceptable. NOTE: The original `ronn` gem is no longer maintained, but development continues in a new `ronn-ng` fork, which we're using here. --- .github/workflows/ci.yml | 4 + .github/workflows/pages.yml | 46 ++++++ .gitignore | 4 + Makefile | 6 +- README.md | 4 +- doc/stackctl-capture.1.md | 57 ------- doc/stackctl-cat.1.md | 35 ----- doc/stackctl-changes.1.md | 57 ------- doc/stackctl-deploy.1.md | 45 ------ doc/stackctl-version.1.md | 19 --- doc/stackctl.1.md | 295 ------------------------------------ man/custom.css | 13 ++ man/index.txt | 8 + man/stackctl-capture.1.ronn | 42 +++++ man/stackctl-cat.1.ronn | 23 +++ man/stackctl-changes.1.ronn | 42 +++++ man/stackctl-deploy.1.ronn | 31 ++++ man/stackctl-version.1.ronn | 10 ++ man/stackctl.1.ronn | 251 ++++++++++++++++++++++++++++++ 19 files changed, 478 insertions(+), 514 deletions(-) create mode 100644 .github/workflows/pages.yml delete mode 100644 doc/stackctl-capture.1.md delete mode 100644 doc/stackctl-cat.1.md delete mode 100644 doc/stackctl-changes.1.md delete mode 100644 doc/stackctl-deploy.1.md delete mode 100644 doc/stackctl-version.1.md delete mode 100644 doc/stackctl.1.md create mode 100644 man/custom.css create mode 100644 man/index.txt create mode 100644 man/stackctl-capture.1.ronn create mode 100644 man/stackctl-cat.1.ronn create mode 100644 man/stackctl-changes.1.ronn create mode 100644 man/stackctl-deploy.1.ronn create mode 100644 man/stackctl-version.1.ronn create mode 100644 man/stackctl.1.ronn diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 85ca0ee..b86d74c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,10 @@ on: push: branches: main +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build: runs-on: ubuntu-latest diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..43c0478 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,46 @@ +name: Pages + +on: + # TODO + # push: + # branches: "main" + pull_request: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: true + +jobs: + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - run: echo "$HOME/.local/share/gem/ruby/3.0.0/bin" >>"$GITHUB_PATH" + - run: gem install --user ronn-ng + - uses: actions/checkout@v3 + + - name: Generate HTML man-pages + run: ronn --style toc,custom --html man/*.ronn + env: + RONN_STYLE: ./man + RONN_ORGANIZATION: Freckle Engineering + + - name: Copy HTML sources to _site + run: | + mkdir -p _site + cp -v man/*.html _site/ + cp -v _site/stackctl.1.html _site/index.html + + - uses: actions/configure-pages@v3 + - uses: actions/upload-pages-artifact@v1 + with: + path: _site + - id: deployment + uses: actions/deploy-pages@v1 diff --git a/.gitignore b/.gitignore index 85e0c44..c42eee2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ *.hie .stack-work dist/ +man/* +!man/index.txt +!man/*.css +!man/*.ronn diff --git a/Makefile b/Makefile index 361c565..5bef2ab 100644 --- a/Makefile +++ b/Makefile @@ -30,11 +30,9 @@ dist/stackctl/completion/%: dist/stackctl/stackctl mkdir -p ./dist/stackctl/completion ./$< --$(@F)-completion-script stackctl > dist/stackctl/completion/$(@F) -PANDOC ?= stack exec pandoc -- - -dist/stackctl/doc/%: doc/%.md +dist/stackctl/doc/%: man/%.ronn mkdir -p ./dist/stackctl/doc - $(PANDOC) --standalone $< --to man >$@ + ronn --organization "Freckle Engineering" --roff <"$<" >"$@" dist/stackctl/Makefile: Makefile mkdir -p dist/stackctl diff --git a/README.md b/README.md index a14ebd6..e095ff6 100644 --- a/README.md +++ b/README.md @@ -72,8 +72,8 @@ Once installed, see: - `man 1 stackctl`, or - `man 1 stackctl ` -The man pages are also available [in-repository](./doc), but contain -documentation as of `main`, and not your installed version. +The man pages are also available [online](https://freckle.github.io/stackctl/), +but contain documentation as of `main`, and not your installed version. ## Relationship to CloudGenesis diff --git a/doc/stackctl-capture.1.md b/doc/stackctl-capture.1.md deleted file mode 100644 index 8ed33a5..0000000 --- a/doc/stackctl-capture.1.md +++ /dev/null @@ -1,57 +0,0 @@ -% STACKCTL-CAPTURE(1) User Manual -% -% March 2022 - -# NAME - -stackctl capture - Generate stack specifications from deployed stacks - -# SYNOPSIS - -*stackctl capture* \[options] - -# DESCRIPTION - -Fetches the CloudFormation Template and currently supplied Parameters of a -deployed Stack and stores it as a stack specification under the -currently-authorized AWS Account and Region. - -If files already exist at the inferred locations, they will be overwritten. - -# OPTIONS - -**\-n**, **\--account-name** *\*\ - -> Write specs paths to **stacks/{account-id}.NAME/...**. If not given, we will -> use **${AWS_PROFILE:-unknown}**. - -**\-t**, **\--template-path** *\*\ - -> Relative path for template. Default is **${STACK}.yaml**. - -**\-p**, **\--path** *\*\ - -> Relative path for specification. Default is **${STACK}.yaml**. - -**\--no-flip**\ - -> Don't flip JSON templates to Yaml. This option is ignored if the template is -> not JSON. - -**STACK**\ - -> Name of Stack to capture. -> -> Globs are also supported and all matching Stacks will be captured. When there -> are multiple Stacks being captured, the **\--path** and **\--template-path** -> will be ignored and all Stacks will be captured to their inferred paths. - -# ENVIRONMENT - -*AWS_PROFILE*\ - -> If set, will be used when defaulting **-n**. - -# STACKCTL - -Part of the **stackctl(1)** suite diff --git a/doc/stackctl-cat.1.md b/doc/stackctl-cat.1.md deleted file mode 100644 index 189b657..0000000 --- a/doc/stackctl-cat.1.md +++ /dev/null @@ -1,35 +0,0 @@ -% STACKCTL-CAT(1) User Manual -% -% March 2022 - -# NAME - -stackctl cat - Pretty-print stack specifications and templates - -# SYNOPSIS - -*stackctl cat* \[options] - -# DESCRIPTION - -This command locates **stacks/** for the currently-authorized AWS Account and -Region and lists them in a tree-like display along with abbreviated contents. It -then lists any **templates/** files used by those stacks in a similar fashion. - -# OPTIONS - -**\--no-stacks**\ - -> Don't print **stacks/**. - -**\--no-templates**\ - -> Don't print **templates/**. - -**\-b**, **\--brief**\ - -> Don't print file contents, only paths. - -# STACKCTL - -Part of the **stackctl(1)** suite diff --git a/doc/stackctl-changes.1.md b/doc/stackctl-changes.1.md deleted file mode 100644 index cd9bd9a..0000000 --- a/doc/stackctl-changes.1.md +++ /dev/null @@ -1,57 +0,0 @@ -% STACKCTL-CHANGES(1) User Manual -% -% March 2022 - -# NAME - -stackctl changes - Create and present Change Sets for stack specifications - -# SYNOPSIS - -*stackctl changes* \[options] - -# DESCRIPTION - -For each stack specification in the currently-active AWS Account and Region, -creates a Change Set and prints it. The Change Set is not removed after -successful operation. - -# OPTIONS - -**\-f**, **\--format** *\*\ - -> Output changes in **FORMAT**. See dedicated section. - -**\--no-include-full**\ - -> Don't include full ChangeSet JSON details. This option only applies to the -> *pr* format. - -**\-p**, **\--parameter** *\*\ - -> Override the given Parameter for this operation. Omitting *VALUE* will result -> in overriding the Parameter as an empty string. May be specified 0 or more -> times. - -**\-t**, **\--tag** *\*\ - -> Override the given Tag for this operation. Omitting *VALUE* will result in -> overriding the Tag as an empty string. May be specified 0 or more times. - -**PATH**\ - -> Write changes to **PATH**, instead of printing them. - -# AVAILABLE FORMATS - -**tty**\ - -> The default. Produces a simplified but colorized (unless redirected) listing. - -**pr**\ - -> Produces markdown suitable to post as a comment to a GitHub Pull Request. - -# STACKCTL - -Part of the **stackctl(1)** suite diff --git a/doc/stackctl-deploy.1.md b/doc/stackctl-deploy.1.md deleted file mode 100644 index 1e69cee..0000000 --- a/doc/stackctl-deploy.1.md +++ /dev/null @@ -1,45 +0,0 @@ -% STACKCTL-DEPLOY(1) User Manual -% -% March 2022 - -# NAME - -stackctl deploy - deploy stack specifications - -# SYNOPSIS - -*stackctl deploy* \[options] - -# DESCRIPTION - -For each stack specification in the currently-active AWS Account and Region, -creates a Change Set and executes it after confirmation. - -# OPTIONS - -**\-p**, **\--parameter** *\*\ - -> Override the given Parameter for this operation. Omitting *VALUE* will result -> in overriding the Parameter as an empty string. May be specified 0 or more -> times. - -**\-t**, **\--tag** *\*\ - -> Override the given Tag for this operation. Omitting *VALUE* will result in -> overriding the Tag as an empty string. May be specified 0 or more times. - -**\--save-change-sets** *\*\ - -> Save generated Change Sets to **PATH/STACK.json** - -**\--no-confirm**\ - -> Don't confirm before deployment. - -**\--clean**\ - -> If successful, remove all Change Sets from the deployed Stack. - -# STACKCTL - -Part of the **stackctl(1)** suite diff --git a/doc/stackctl-version.1.md b/doc/stackctl-version.1.md deleted file mode 100644 index 1fecd87..0000000 --- a/doc/stackctl-version.1.md +++ /dev/null @@ -1,19 +0,0 @@ -% STACKCTL-VERSION(1) User Manual -% -% March 2022 - -# NAME - -stackctl version - Display version information about Stackctl - -# SYNOPSIS - -*stackctl version* \[options] - -# OPTIONS - -None. - -# STACKCTL - -Part of the **stackctl(1)** suite diff --git a/doc/stackctl.1.md b/doc/stackctl.1.md deleted file mode 100644 index 185afa6..0000000 --- a/doc/stackctl.1.md +++ /dev/null @@ -1,295 +0,0 @@ -% STACKCTL(1) User Manual -% -% January 2023 - -# NAME - -stackctl - manage CloudFormation Stacks through specifications - -# SYNOPSIS - -*stackctl* \[options] \ \ - -# OPTIONS - -**\-d**, **\--directory** *\*\ - -> Where to find specifications. Default is **.**. - -**\--filter** *\*\ - -> Restrict specifications to those whose paths match any of the given -> **PATTERN**s. - -**\--color** *\*\ - -> When to colorize output. **auto** (the default) will colorize output when -> connected to a terminal. - -**\-v**, **\--verbose**\ - -> Log more verbosely - -# COMMANDS - -**cat**\ - -> Pretty-print specifications. - -**capture**\ - -> Generate specifications for already-deployed Stacks. - -**changes**\ - -> Show changes between on-disk specifications and their deployed state. - -**deploy**\ - -> Make deployed state match on-disk specifications. - -**version**\ - -> Print the CLI's version. - -Run **man stackctl \** for more details. - -# Stack Specifications - -A *Stack Specification* is a file format and file-system structure used to fully -describe a deployed (or deployable) CloudFormation Stack. *stackctl* is your way -of creating, displaying, and using such files. - -## Format - -Specification files ("specs") have the following path structure: - -> stacks/*{account-id}*.*{account-name}*/*{region}*/*{stack-name}*.yaml - -Its constituent parts are used as follows: - -*{account-id}*\ - -> The AWS Account Id in which to deploy this Stack. - -*{account-name}*\ - -> A friendly name for this Account. This is never used logically and can be - whatever you find useful for identifying this Account. - -*{region}*\ - -> The AWS Region in which to deploy this Stack - -*{stack-name}*\ - -> The name to use for this Stack. -> -> *{stack/name}*.yaml is also supported so that directories can be used for -> your own organization. Such paths will have directory-separators replaced by -> hyphens when used. - -These files' contents should be: - -``` -Description: - -Template: - -Depends: - - - -Actions: - - on: - run: - : - -Parameters: Object - -Capabilities: - - - -Tags: Object -``` - -And these constituent parts are used as follows: - -*{.Description}*\ - -> Optional. Set the Stack's description. -> -> This value will be inserted as the *Description* key in the template body on -> deployment, which becomes the deployed Stack's description. If the template -> already contains a description, the specification value will be ignored. - -*{.Template}*\ - -> Required. The template to use when deploying this Stack. Must be a relative -> path under `templates/`. - -*{.Depends}*\ - -> Optional. Other Stacks (by name) that should be ordered before this one if -> deployed together. - -*{.Actions}*\ - -> Optional. Actions to run when certain Stack management events occur. - -*{.Actions[].on}*\ - -> The event on which to perform the action: -> -> - **PostDeploy**: run the action after a successful deployment - -*{.Actions[].run}*\ - -> The action to perform on the given event: -> -> - **InvokeLambdaByStackOutput**: *\*: invoke the function whose -> name is found in the given Output of the deployed Stack -> - **InvokeLambdaByName**: *\*: invoke the given function - -*{.Parameters}*\ - -> Optional. Parameters to use when deploying the Stack. -> -> The *Parameters* key can be specified in any of 3 forms: -> -> ``` -> # Natural (recommended) -> Parameters: -> Foo: Bar -> Baz: Bat -> -> # CloudFormation -> Parameters: -> - ParameterKey: Foo -> ParameterValue: Bar -> - ParameterKey: Baz -> ParameterValue: Bat -> -> # CloudGenesis -> Parameters: -> - Key: Foo -> Value: Bar -> - Key: Baz -> Value: Bat -> ``` - -*{.Capabilities}*\ - -> Optional. Capabilities to use when deploying the Stack. -> -> Valid *Capabilities* are, -> -> **CAPABILITY_AUTO_EXPAND**,\ -> **CAPABILITY_IAM**, and\ -> **CAPABILITY_NAMED_IAM** - -*{.Tags}*\ - -> Optional. Tags to use when deploying the Stack. -> -> The *Tags* key can be specified in either of 2 forms: -> -> ``` -> # Natural (recommended) -> Tags: -> Foo: Bar -> Baz: Bat -> -> # CloudFormation / CloudGenesis -> Parameters: -> - Key: Foo -> Value: Bar -> - Key: Baz -> Value: Bat -> ``` - -## Example - -The following example shares a single Template between two deployments in two -regions of a single account. - -``` -stacks/ - 111111111111.prod/ - us-east-1/ - my-app.yaml - | Template: web.yaml - | Parameters: - | ... - - us-west-2/ - my-app.yaml - | Template: web.yaml - | Parameters: - | ... - -templates/ - web.yaml - | Parameters: - | ... - | Resources: - | ... -``` - -## Deployment - -Once we have a specification, deployment is *conceptually* simple: - -```sh -aws configure # for {account-id} - -aws --region {region} cloudformation deploy \ - --stack-name {stack-name} \ - --template-file templates/{.Template} \ - --parameter-overrides {.Parameters} \ - --capabilities {.Capabilities} \ - --tags {.Tags} -``` - -In reality, we create changesets, optionally present them for review, execute -them, wait, stream events, and finally clean up. - -See **stackctl-changes(1)** and **stackctl-deploy(1)**. - -# ENVIRONMENT - -*STACKCTL_DIRECTORY*\ - -> Environment-based alternative for *\--directory*. - -*STACKCTL_FILTER*\ - -> Environment-based alternative for *\--filter*. - -*LOG_\**\ - -> Variables such as *LOG_COLOR* or *LOG_LEVEL* will be respected by the -> underlying logging framework (Blammo). Please see its documentation for -> complete details: -> -> https://github.com/freckle/blammo#configuration - -*AWS_PROFILE*\ - -> If set, will be used as account name in commands that create new -> specifications. - -# AUTHOR - -Freckle Engineering - -# SEE ALSO - -**stackctl-cat(1)**, **stackctl-capture(1)**, **stackctl-changes(1)**, -**stackctl-deploy(1)**, **stackctl-version(1)**. - -# ACKNOWLEDGEMENTS - -The specification format and semantics is a minor extension of that used by the -CloudGenesis project, capturing more of a CloudFormation Stack's deployed state -statically is terraform-inspired, and GitOps as an approach was pioneered for -Kubernetes by Flux CD. diff --git a/man/custom.css b/man/custom.css new file mode 100644 index 0000000..c12e532 --- /dev/null +++ b/man/custom.css @@ -0,0 +1,13 @@ +.mp h1, +.mp h2, +.mp h3, +.mp h4, +.mp h5, +.mp h6, +.mp code { + color: #A00000; +} + +.mp var { + color: #006000; +} diff --git a/man/index.txt b/man/index.txt new file mode 100644 index 0000000..793d09f --- /dev/null +++ b/man/index.txt @@ -0,0 +1,8 @@ +# manuals included in this project: +stackctl-cat(1) stackctl-cat.1.ronn +stackctl-capture(1) stackctl-capture.1.ronn +stackctl-changes(1) stackctl-changes.1.ronn +stackctl-deploy(1) stackctl-deploy.1.ronn +stackctl-version(1) stackctl-version.1.ronn + +# external manuals diff --git a/man/stackctl-capture.1.ronn b/man/stackctl-capture.1.ronn new file mode 100644 index 0000000..9239cd8 --- /dev/null +++ b/man/stackctl-capture.1.ronn @@ -0,0 +1,42 @@ +stackctl-capture(1) - Generate stack specifications from deployed stacks +======================================================================== + +## SYNOPSIS + +`stackctl capture` [] + +## DESCRIPTION + +Fetches the CloudFormation Template and currently supplied Parameters of a +deployed Stack and stores it as a stack specification under the +currently-authorized AWS Account and Region. + +If files already exist at the inferred locations, they will be overwritten. + +## OPTIONS + + * `-n`, `--account-name`=: + Write specs paths to `stacks/{account-id}.NAME/...`. If not given, we will + use `${AWS_PROFILE:-unknown}`. + + * `-t`, `--template-path`=: + Relative path for template. Default is `${STACK}.yaml`. + + * `-p`, `--path`=: + Relative path for specification. Default is `${STACK}.yaml`. + + * `--no-flip`: + Don't flip JSON templates to Yaml. This option is ignored if the template is + not JSON. + + * `STACK`: + Name of Stack to capture. + + Globs are also supported and all matching Stacks will be captured. When + there are multiple Stacks being captured, the `--path` and `--template-path` + will be ignored and all Stacks will be captured to their inferred paths. + +## ENVIRONMENT + + * `AWS_PROFILE`: + If set, will be used when defaulting `-n`. diff --git a/man/stackctl-cat.1.ronn b/man/stackctl-cat.1.ronn new file mode 100644 index 0000000..3d0b1bb --- /dev/null +++ b/man/stackctl-cat.1.ronn @@ -0,0 +1,23 @@ +stackctl-cat(1) - pretty-print stack specifications and templates +================================================================= + +## SYNOPSIS + +`stackctl cat` [] + +## DESCRIPTION + +This command locates `stacks/` for the currently-authorized AWS Account and +Region and lists them in a tree-like display along with abbreviated contents. It +then lists any `templates/` files used by those stacks in a similar fashion. + +## OPTIONS + + * `--no-stacks`: + Don't print `stacks/`. + + * `--no-templates`: + Don't print `templates/`. + + * `-b`, `--brief`: + Don't print file contents, only paths. diff --git a/man/stackctl-changes.1.ronn b/man/stackctl-changes.1.ronn new file mode 100644 index 0000000..5f3fa22 --- /dev/null +++ b/man/stackctl-changes.1.ronn @@ -0,0 +1,42 @@ +stackctl-changes(1) - create and present Change Sets for stack specifications +============================================================================= + +## SYNOPSIS + +`stackctl changes` [] + +## DESCRIPTION + +For each stack specification in the currently-active AWS Account and Region, +creates a Change Set and prints it. The Change Set is not removed after +successful operation. + +## OPTIONS + + * `-f`, `--format`=: + Output changes in . See dedicated section. + + * `--no-include-full`: + Don't include full Change Set JSON details. This option only applies to the + format. + + * `-p`, `--parameter`=: + Override the given Parameter for this operation. Omitting will + result in overriding the Parameter as an empty string. May be specified 0 or + more times. + + * `-t`, `--tag`=: + Override the given Tag for this operation. Omitting will result in + overriding the Tag as an empty string. May be specified 0 or more times. + + * `PATH`: + Write changes to , instead of printing them. + +## AVAILABLE FORMATS + + * `tty`: + The default. Produces a simplified but colorized (unless redirected) + listing. + + * `pr`: + Produces markdown suitable to post as a comment to a GitHub Pull Request. diff --git a/man/stackctl-deploy.1.ronn b/man/stackctl-deploy.1.ronn new file mode 100644 index 0000000..d5514cc --- /dev/null +++ b/man/stackctl-deploy.1.ronn @@ -0,0 +1,31 @@ +stackctl-deploy(1) - deploy stack specifications +================================================ + +## SYNOPSIS + +`stackctl deploy` [] + +## DESCRIPTION + +For each stack specification in the currently-active AWS Account and Region, +creates a Change Set and executes it after confirmation. + +## OPTIONS + + * `-p`, `--parameter`=: + Override the given Parameter for this operation. Omitting will + result in overriding the Parameter as an empty string. May be specified 0 or + more times. + + * `-t`, `--tag`=: + Override the given Tag for this operation. Omitting will result in + overriding the Tag as an empty string. May be specified 0 or more times. + + * `--save-change-sets`=: + Save generated Change Sets to `{PATH}/{STACK}.json`. + + * `--no-confirm`: + Don't confirm before deployment. + + * `--clean`: + If successful, remove all Change Sets from the deployed Stack. diff --git a/man/stackctl-version.1.ronn b/man/stackctl-version.1.ronn new file mode 100644 index 0000000..1ab44f6 --- /dev/null +++ b/man/stackctl-version.1.ronn @@ -0,0 +1,10 @@ +stackctl-version(1) - display version information about Stackctl +================================================================ + +## SYNOPSIS + +`stackctl version` + +## OPTIONS + +None. diff --git a/man/stackctl.1.ronn b/man/stackctl.1.ronn new file mode 100644 index 0000000..d57bba0 --- /dev/null +++ b/man/stackctl.1.ronn @@ -0,0 +1,251 @@ +stackctl(1) - manage CloudFormation Stacks through specifications +================================================================= + +## SYNOPSIS + +`stackctl` [] + +## OPTIONS + + * `-d`, `--directory`=: + Where to find specifications. Default is `.`. + + * `--filter`=: + Restrict specifications to those whose paths match any given . + + * `--color`=: + When to colorize output. `auto` (the default) will colorize output when + connected to a terminal. + + * `-v`, `--verbose`: + Log more verbosely + +## COMMANDS + + * `cat`: + Pretty-print specifications. + + * `capture`: + Generate specifications for already-deployed Stacks. + + * `changes`: + Show changes between on-disk specifications and their deployed state. + + * `deploy`: + Make deployed state match on-disk specifications. + + * `version`: + Print the CLI's version. + +Run `man stackctl ` for more details. + +## STACK SPECIFICATIONS + +A *Stack Specification* is a file format and file-system structure used to fully +describe a deployed (or deployable) CloudFormation Stack. *stackctl* is your way +of creating, displaying, and using such files. + +### FORMAT + +Specification files ("specs") have the following path structure: + + stacks/{account-id}.{account-name}/{region}/{stack-name}.yaml + +Its constituent parts are used as follows: + + * `{account-id}`: + The AWS Account Id in which to deploy this Stack. + + * `{account-name}`: + A friendly name for this Account. This is never used logically and can be + whatever you find useful for identifying this Account. + + * `{region}`: + The AWS Region in which to deploy this Stack. + + * `{stack-name}`: + The name to use for this Stack. + + `{stack/name}`.yaml is also supported, so that directories can be used for + your own organization. Such paths will have directory-separators replaced by + hyphens when used. + +These files' contents should be: + + Description: + + Template: + + Depends: + - + + Actions: + - on: + run: + : + + Parameters: Object + + Capabilities: + - + + Tags: Object + +And these constituent parts are used as follows: + + * `{.Description}`: + Optional. Set the Stack's description. + + This value will be inserted as the *Description* key in the template body on + deployment, which becomes the deployed Stack's description. If the template + already contains a description, the specification value will be ignored. + + * `{.Template}`: + Required. The template to use when deploying this Stack. Must be a relative + path under `templates/`. + + * `{.Depends}`: + Optional. Other Stacks (by name) that should be ordered before this one if + deployed together. + + * `{.Actions}`: + Optional. Actions to run when certain Stack management events occur. + + * `{.Actions[].on}`: + The event on which to perform the action: + + **PostDeploy**: run the action after a successful deployment. + + * `{.Actions[].run}`: + The action to perform on the given event: + + **InvokeLambdaByStackOutput**: : invoke the function whose name + is found in the given Output of the deployed Stack. + + **InvokeLambdaByName**: : invoke the given function. + + * `{.Parameters}`: + Optional. Parameters to use when deploying the Stack. + + The _Parameters_ key can be specified in any of 3 forms: + + # Natural (recommended) + Parameters: + Foo: Bar + Baz: Bat + + # CloudFormation + Parameters: + - ParameterKey: Foo + ParameterValue: Bar + - ParameterKey: Baz + ParameterValue: Bat + + # CloudGenesis + Parameters: + - Key: Foo + Value: Bar + - Key: Baz + Value: Bat + + * `{.Capabilities}`: + Optional. Capabilities to use when deploying the Stack. + + Valid _Capabilities_ are **CAPABILITY_AUTO_EXPAND**, **CAPABILITY_IAM**, and + **CAPABILITY_NAMED_IAM**. + + * `{.Tags}`: + Optional. Tags to use when deploying the Stack. + + The _Tags_ key can be specified in either of 2 forms: + + # Natural (recommended) + Tags: + Foo: Bar + Baz: Bat + + # CloudFormation / CloudGenesis + Parameters: + - Key: Foo + Value: Bar + - Key: Baz + Value: Bat + +## EXAMPLE + +The following example shares a single Template between two deployments in two +regions of a single account. + + stacks/ + 111111111111.prod/ + us-east-1/ + my-app.yaml + | Template: web.yaml + | Parameters: + | ... + + us-west-2/ + my-app.yaml + | Template: web.yaml + | Parameters: + | ... + + templates/ + web.yaml + | Parameters: + | ... + | Resources: + | ... + +## DEPLOYMENT + +Once we have a specification, deployment is _conceptually_ simple: + + aws configure # for {account-id} + + aws --region {region} cloudformation deploy \ + --stack-name {stack-name} \ + --template-file templates/{.Template} \ + --parameter-overrides {.Parameters} \ + --capabilities {.Capabilities} \ + --tags {.Tags} + +In reality, we create changesets, optionally present them for review, execute +them, wait, stream events, and finally clean up. + +See stackctl-changes(1) and stackctl-deploy(1). + +## ENVIRONMENT + +* `STACKCTL_DIRECTORY`: + Environment-based alternative for `--directory`. + +* `STACKCTL_FILTER`: + Environment-based alternative for `--filter`. + +* `LOG_*`: + Variables such as *LOG_COLOR* or *LOG_LEVEL* will be respected by the + underlying logging framework (Blammo). Please see [its documentation][blammo] + for complete details. + + [blammo]: https://github.com/freckle/blammo#configuration + +* `AWS_PROFILE`: + If set, will be used as in commands that create new + specifications. + +## AUTHOR + +Freckle Engineering + +## SEE ALSO + +stackctl-cat(1), stackctl-capture(1), stackctl-changes(1), stackctl-deploy(1), +stackctl-version(1). + +## ACKNOWLEDGEMENTS + +The specification format and semantics is a minor extension of that used by the +CloudGenesis project, capturing more of a CloudFormation Stack's deployed state +statically is terraform-inspired, and GitOps as an approach was pioneered for +Kubernetes by Flux CD. From 521a0aade7c568c67efc0adc35663d881dacda2e Mon Sep 17 00:00:00 2001 From: Pat Brisbin Date: Tue, 11 Apr 2023 17:20:40 -0400 Subject: [PATCH 059/187] Fix TODO in pages workflow We're no longer testing it via PR events. Pushes to main only. --- .github/workflows/pages.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 43c0478..301e921 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -1,10 +1,8 @@ name: Pages on: - # TODO - # push: - # branches: "main" - pull_request: + push: + branches: "main" permissions: contents: read From 9c1a5ccacf60c13a67177efbdaabcb5b56603eda Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Thu, 20 Apr 2023 10:41:46 -0400 Subject: [PATCH 060/187] Add stackctl-ls(1) Presents a simple list of stacks and indicates if a deployed Stack exists or not. ![](https://files.pbrisbin.com/screenshots/screenshot.2910388.png) This was useful to me for finding some old Stacks that were manually deleted, so I could remove the corresponding files in our infra repository. --- app/Main.hs | 1 + man/stackctl-ls.1.ronn | 19 ++++++++++++ man/stackctl.1.ronn | 5 +++- src/Stackctl/Commands.hs | 21 +++++++++---- src/Stackctl/Spec/List.hs | 62 +++++++++++++++++++++++++++++++++++++++ src/Stackctl/StackSpec.hs | 10 ++++--- stackctl.cabal | 1 + 7 files changed, 109 insertions(+), 10 deletions(-) create mode 100644 man/stackctl-ls.1.ronn create mode 100644 src/Stackctl/Spec/List.hs diff --git a/app/Main.hs b/app/Main.hs index 90e0b00..cecba33 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -14,4 +14,5 @@ main = <> subcommand Commands.capture <> subcommand Commands.changes <> subcommand Commands.deploy + <> subcommand Commands.list <> subcommand Commands.version diff --git a/man/stackctl-ls.1.ronn b/man/stackctl-ls.1.ronn new file mode 100644 index 0000000..92472cb --- /dev/null +++ b/man/stackctl-ls.1.ronn @@ -0,0 +1,19 @@ +stackctl-ls(1) - list stack specifications +========================================== + +## SYNOPSIS + +`stackctl ls` + +## DESCRIPTION + +This command locates `stacks/` for the currently-authorized AWS Account and +Region and lists them. + +The key differences between this and stackctl-cat(1) is that this command lists +things as simple rows and indicates for each spec if a deployed stack exists in +the first column. + +## OPTIONS + +None. diff --git a/man/stackctl.1.ronn b/man/stackctl.1.ronn index d57bba0..4ca9cd7 100644 --- a/man/stackctl.1.ronn +++ b/man/stackctl.1.ronn @@ -34,6 +34,9 @@ stackctl(1) - manage CloudFormation Stacks through specifications * `deploy`: Make deployed state match on-disk specifications. + * `ls`: + List specifications. + * `version`: Print the CLI's version. @@ -241,7 +244,7 @@ Freckle Engineering ## SEE ALSO stackctl-cat(1), stackctl-capture(1), stackctl-changes(1), stackctl-deploy(1), -stackctl-version(1). +stackctl-ls(1), stackctl-version(1). ## ACKNOWLEDGEMENTS diff --git a/src/Stackctl/Commands.hs b/src/Stackctl/Commands.hs index 2d07e5b..864f3f8 100644 --- a/src/Stackctl/Commands.hs +++ b/src/Stackctl/Commands.hs @@ -1,9 +1,5 @@ module Stackctl.Commands - ( cat - , capture - , changes - , deploy - , version + ( module Stackctl.Commands ) where import Stackctl.Prelude @@ -15,6 +11,7 @@ import Stackctl.Spec.Capture import Stackctl.Spec.Cat import Stackctl.Spec.Changes import Stackctl.Spec.Deploy +import Stackctl.Spec.List import Stackctl.Subcommand import Stackctl.VerboseOption import Stackctl.Version @@ -74,6 +71,20 @@ deploy = Subcommand , run = runAppSubcommand runDeploy } +list + :: ( HasColorOption options + , HasVerboseOption options + , HasDirectoryOption options + , HasFilterOption options + ) + => Subcommand options ListOptions +list = Subcommand + { name = "ls" + , description = "List specifications" + , parse = parseListOptions + , run = runAppSubcommand runList + } + version :: Subcommand options () version = Subcommand { name = "version" diff --git a/src/Stackctl/Spec/List.hs b/src/Stackctl/Spec/List.hs new file mode 100644 index 0000000..d96d117 --- /dev/null +++ b/src/Stackctl/Spec/List.hs @@ -0,0 +1,62 @@ +module Stackctl.Spec.List + ( ListOptions(..) + , parseListOptions + , runList + ) where + +import Stackctl.Prelude + +import Blammo.Logging.Logger (pushLoggerLn) +import Options.Applicative +import Stackctl.AWS +import Stackctl.AWS.Scope +import Stackctl.Colors +import Stackctl.Config (HasConfig) +import Stackctl.DirectoryOption (HasDirectoryOption(..)) +import Stackctl.FilterOption (HasFilterOption) +import Stackctl.Spec.Discover +import Stackctl.StackSpec + +data ListOptions = ListOptions + +-- brittany-disable-next-binding + +parseListOptions :: Parser ListOptions +parseListOptions = pure ListOptions + +runList + :: ( MonadUnliftIO m + , MonadMask m + , MonadResource m + , MonadLogger m + , MonadReader env m + , HasAwsScope env + , HasAwsEnv env + , HasLogger env + , HasConfig env + , HasDirectoryOption env + , HasFilterOption env + ) + => ListOptions + -> m () +runList _ = do + specs <- discoverSpecs + Colors {..} <- getColorsLogger + + for_ specs $ \spec -> do + let + path = stackSpecFilePath spec + name = stackSpecStackName spec + + exists <- isJust <$> awsCloudFormationDescribeStackMaybe name + + let + formatted :: Text + formatted = + " " + <> (if exists then green "✓ " else yellow "✗ ") + <> cyan (unStackName name) + <> " => " + <> magenta (pack path) + + pushLoggerLn formatted diff --git a/src/Stackctl/StackSpec.hs b/src/Stackctl/StackSpec.hs index 3de9acd..c7e7d55 100644 --- a/src/Stackctl/StackSpec.hs +++ b/src/Stackctl/StackSpec.hs @@ -1,5 +1,6 @@ module Stackctl.StackSpec ( StackSpec + , stackSpecFilePath , stackSpecSpecPath , stackSpecSpecBody , stackSpecStackName @@ -45,6 +46,10 @@ data StackSpec = StackSpec stackSpecSpecRoot :: StackSpec -> FilePath stackSpecSpecRoot = ssSpecRoot +stackSpecFilePath :: StackSpec -> FilePath +stackSpecFilePath spec = + FilePath.normalise $ stackSpecSpecRoot spec stackSpecStackFile spec + stackSpecSpecPath :: StackSpec -> StackSpecPath stackSpecSpecPath = ssSpecPath @@ -160,10 +165,7 @@ writeStackSpec overwrite stackSpec mTemplateBody = do liftIO $ Yaml.encodeFile specPath $ stackSpecSpecBody stackSpec where templatePath = unStackTemplate $ stackSpecTemplate stackSpec - specPath = - FilePath.normalise - $ stackSpecSpecRoot stackSpec - stackSpecStackFile stackSpec + specPath = stackSpecFilePath stackSpec readStackSpec :: (MonadIO m, MonadReader env m, HasConfig env) diff --git a/stackctl.cabal b/stackctl.cabal index 02b4239..846ef54 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -55,6 +55,7 @@ library Stackctl.Spec.Deploy Stackctl.Spec.Discover Stackctl.Spec.Generate + Stackctl.Spec.List Stackctl.StackDescription Stackctl.StackSpec Stackctl.StackSpecPath From dfd98b9007b0de9bd4ff1929fb20484a8ff03d03 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 23 May 2023 09:38:48 -0400 Subject: [PATCH 061/187] Add --auto-sso option to invoke aws-sso-login if necessary --- man/stackctl.1.ronn | 7 ++++ package.yaml | 2 + src/Stackctl/AutoSSO.hs | 81 ++++++++++++++++++++++++++++++++++++++ src/Stackctl/CLI.hs | 7 +++- src/Stackctl/Commands.hs | 6 +++ src/Stackctl/Options.hs | 10 +++++ src/Stackctl/Prompt.hs | 9 ++++- src/Stackctl/Subcommand.hs | 6 ++- stackctl.cabal | 3 ++ 9 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 src/Stackctl/AutoSSO.hs diff --git a/man/stackctl.1.ronn b/man/stackctl.1.ronn index 4ca9cd7..fd6782d 100644 --- a/man/stackctl.1.ronn +++ b/man/stackctl.1.ronn @@ -20,6 +20,10 @@ stackctl(1) - manage CloudFormation Stacks through specifications * `-v`, `--verbose`: Log more verbosely + * `--auto-sso`=: + When to automatically run `aws sso login` in response to AWS SSO + authorization errors. `always`, `ask`, or `never`. Default is to `ask`. + ## COMMANDS * `cat`: @@ -226,6 +230,9 @@ See stackctl-changes(1) and stackctl-deploy(1). * `STACKCTL_FILTER`: Environment-based alternative for `--filter`. +* `STACKCTL_AUTO_SSO`: + Environment-based alternative for `--auto-sso`. + * `LOG_*`: Variables such as *LOG_COLOR* or *LOG_LEVEL* will be respected by the underlying logging framework (Blammo). Please see [its documentation][blammo] diff --git a/package.yaml b/package.yaml index a596a8c..641b6c6 100644 --- a/package.yaml +++ b/package.yaml @@ -69,6 +69,7 @@ library: - amazonka-core - amazonka-ec2 - amazonka-lambda + - amazonka-sso - amazonka-sts - bytestring - cfn-flip >= 0.1.0.3 # bugfix for Condition @@ -90,6 +91,7 @@ library: - text - time - transformers + - typed-process - unliftio - unliftio-core - unordered-containers diff --git a/src/Stackctl/AutoSSO.hs b/src/Stackctl/AutoSSO.hs new file mode 100644 index 0000000..006def9 --- /dev/null +++ b/src/Stackctl/AutoSSO.hs @@ -0,0 +1,81 @@ +module Stackctl.AutoSSO + ( AutoSSOOption + , defaultAutoSSOOption + , HasAutoSSOOption(..) + , autoSSOOption + , envAutoSSOOption + , handleAutoSSO + ) where + +import Stackctl.Prelude + +import Amazonka.SSO (_UnauthorizedException) +import Amazonka.Types (Error, ErrorMessage(..), serviceMessage) +import Data.Semigroup (Last(..)) +import qualified Env +import Options.Applicative +import Stackctl.Prompt +import System.Process.Typed + +data AutoSSOOption + = AutoSSOAlways + | AutoSSOAsk + | AutoSSONever + deriving Semigroup via Last AutoSSOOption + +defaultAutoSSOOption :: AutoSSOOption +defaultAutoSSOOption = AutoSSOAsk + +readAutoSSO :: String -> Either String AutoSSOOption +readAutoSSO = \case + "always" -> Right AutoSSOAlways + "ask" -> Right AutoSSOAsk + "never" -> Right AutoSSONever + x -> + Left $ "Invalid choice for auto-sso: " <> x <> ", must be always|ask|never" + +class HasAutoSSOOption env where + autoSSOOptionL :: Lens' env AutoSSOOption + +autoSSOOption :: Parser AutoSSOOption +autoSSOOption = option (eitherReader readAutoSSO) + $ mconcat [long "auto-sso", help autoSSOHelp, metavar "WHEN"] + +envAutoSSOOption :: Env.Parser Env.Error AutoSSOOption +envAutoSSOOption = Env.var (first Env.UnreadError . readAutoSSO) "AUTO_SSO" + $ Env.help autoSSOHelp + +autoSSOHelp :: IsString a => a +autoSSOHelp = "Automatically run aws-sso-login if necessary?" + +handleAutoSSO + :: ( MonadUnliftIO m + , MonadReader env m + , MonadLogger m + , HasLogger env + , HasAutoSSOOption options + ) + => options + -> m a + -> m a +handleAutoSSO options f = do + catchJust (preview (_UnauthorizedException @Error)) f $ \ex -> do + case options ^. autoSSOOptionL of + AutoSSOAlways -> do + logWarn $ ssoErrorMessage ex + logInfo "Running `aws sso login' automatically" + AutoSSOAsk -> do + logWarn $ ssoErrorMessage ex + promptOrExit "Run `aws sso login'" + AutoSSONever -> do + logError $ ssoErrorMessage ex + exitFailure + + runProcess_ $ proc "aws" ["sso", "login"] + f + where + ssoErrorMessage ex = + "AWS SSO authorization error" + :# [ "message" .= fmap fromErrorMessage (ex ^. serviceMessage) + , "hint" .= ("Run `aws sso login' and try again" :: Text) + ] diff --git a/src/Stackctl/CLI.hs b/src/Stackctl/CLI.hs index efd5d2c..5dec511 100644 --- a/src/Stackctl/CLI.hs +++ b/src/Stackctl/CLI.hs @@ -10,6 +10,7 @@ import Stackctl.Prelude import qualified Blammo.Logging.LogSettings.Env as LoggingEnv import Control.Monad.Catch (MonadCatch) import Control.Monad.Trans.Resource (ResourceT, runResourceT) +import Stackctl.AutoSSO import Stackctl.AWS import Stackctl.AWS.Scope import Stackctl.ColorOption @@ -53,6 +54,9 @@ instance HasColorOption options => HasColorOption (App options) where instance HasVerboseOption options => HasVerboseOption (App options) where verboseOptionL = optionsL . verboseOptionL +instance HasAutoSSOOption options => HasAutoSSOOption (App options) where + autoSSOOptionL = optionsL . autoSSOOptionL + newtype AppT app m a = AppT { unAppT :: ReaderT app (LoggingT (ResourceT m)) a } @@ -75,6 +79,7 @@ runAppT , MonadUnliftIO m , HasColorOption options , HasVerboseOption options + , HasAutoSSOOption options ) => options -> AppT (App options) m a @@ -92,7 +97,7 @@ runAppT options f = do envLogSettings app <- runResourceT $ runLoggerLoggingT logger $ do - aws <- awsEnvDiscover + aws <- runReaderT (handleAutoSSO options awsEnvDiscover) logger App logger <$> loadConfigOrExit diff --git a/src/Stackctl/Commands.hs b/src/Stackctl/Commands.hs index 864f3f8..2a24e17 100644 --- a/src/Stackctl/Commands.hs +++ b/src/Stackctl/Commands.hs @@ -4,6 +4,7 @@ module Stackctl.Commands import Stackctl.Prelude +import Stackctl.AutoSSO import Stackctl.ColorOption import Stackctl.DirectoryOption import Stackctl.FilterOption @@ -21,6 +22,7 @@ cat , HasVerboseOption options , HasDirectoryOption options , HasFilterOption options + , HasAutoSSOOption options ) => Subcommand options CatOptions cat = Subcommand @@ -34,6 +36,7 @@ capture :: ( HasColorOption options , HasVerboseOption options , HasDirectoryOption options + , HasAutoSSOOption options ) => Subcommand options CaptureOptions capture = Subcommand @@ -48,6 +51,7 @@ changes , HasVerboseOption options , HasDirectoryOption options , HasFilterOption options + , HasAutoSSOOption options ) => Subcommand options ChangesOptions changes = Subcommand @@ -62,6 +66,7 @@ deploy , HasVerboseOption options , HasDirectoryOption options , HasFilterOption options + , HasAutoSSOOption options ) => Subcommand options DeployOptions deploy = Subcommand @@ -76,6 +81,7 @@ list , HasVerboseOption options , HasDirectoryOption options , HasFilterOption options + , HasAutoSSOOption options ) => Subcommand options ListOptions list = Subcommand diff --git a/src/Stackctl/Options.hs b/src/Stackctl/Options.hs index 26d6675..f9b279b 100644 --- a/src/Stackctl/Options.hs +++ b/src/Stackctl/Options.hs @@ -9,6 +9,7 @@ import Stackctl.Prelude import Data.Semigroup.Generic import qualified Env import Options.Applicative +import Stackctl.AutoSSO import Stackctl.ColorOption import Stackctl.DirectoryOption import Stackctl.FilterOption @@ -19,6 +20,7 @@ data Options = Options , oFilter :: Maybe FilterOption , oColor :: Maybe ColorOption , oVerbose :: Verbosity + , oAutoSSO :: Maybe AutoSSOOption } deriving stock Generic deriving Semigroup via GenericSemigroupMonoid Options @@ -29,6 +31,9 @@ directoryL = lens oDirectory $ \x y -> x { oDirectory = y } filterL :: Lens' Options (Maybe FilterOption) filterL = lens oFilter $ \x y -> x { oFilter = y } +autoSSOL :: Lens' Options (Maybe AutoSSOOption) +autoSSOL = lens oAutoSSO $ \x y -> x { oAutoSSO = y } + instance HasDirectoryOption Options where directoryOptionL = directoryL . maybeLens defaultDirectoryOption @@ -41,6 +46,9 @@ instance HasColorOption Options where instance HasVerboseOption Options where verboseOptionL = lens oVerbose $ \x y -> x { oVerbose = y } +instance HasAutoSSOOption Options where + autoSSOOptionL = autoSSOL . maybeLens defaultAutoSSOOption + -- brittany-disable-next-binding envParser :: Env.Parser Env.Error Options @@ -49,6 +57,7 @@ envParser = Env.prefixed "STACKCTL_" $ Options <*> optional (envFilterOption "specifications") <*> pure mempty -- use LOG_COLOR <*> pure mempty -- use LOG_LEVEL + <*> optional envAutoSSOOption -- brittany-disable-next-binding @@ -58,3 +67,4 @@ optionsParser = Options <*> optional (filterOption "specifications") <*> optional colorOption <*> verboseOption + <*> optional autoSSOOption diff --git a/src/Stackctl/Prompt.hs b/src/Stackctl/Prompt.hs index d006bf5..ad3825e 100644 --- a/src/Stackctl/Prompt.hs +++ b/src/Stackctl/Prompt.hs @@ -1,6 +1,7 @@ module Stackctl.Prompt ( prompt , promptContinue + , promptOrExit ) where import Stackctl.Prelude @@ -34,7 +35,13 @@ prompt message parse dispatch = do promptContinue :: (MonadIO m, MonadLogger m, MonadReader env m, HasLogger env) => m () -promptContinue = prompt "Continue (y/n)" parse dispatch +promptContinue = promptOrExit "Continue" + +promptOrExit + :: (MonadIO m, MonadLogger m, MonadReader env m, HasLogger env) + => Text + -> m () +promptOrExit msg = prompt (msg <> " (y/n)") parse dispatch where parse x | x `elem` ["y", "Y"] = Right True diff --git a/src/Stackctl/Subcommand.hs b/src/Stackctl/Subcommand.hs index 928c9e4..ac296e6 100644 --- a/src/Stackctl/Subcommand.hs +++ b/src/Stackctl/Subcommand.hs @@ -10,6 +10,7 @@ import Stackctl.Prelude import qualified Env import Options.Applicative +import Stackctl.AutoSSO import Stackctl.CLI import Stackctl.ColorOption import Stackctl.Options @@ -61,7 +62,10 @@ runSubcommand' title parseEnv parseCLI sp = do -- @ -- runAppSubcommand - :: (HasColorOption options, HasVerboseOption options) + :: ( HasColorOption options + , HasVerboseOption options + , HasAutoSSOOption options + ) => (subOptions -> AppT (App options) IO a) -> subOptions -> options diff --git a/stackctl.cabal b/stackctl.cabal index 846ef54..dca1673 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -26,6 +26,7 @@ source-repository head library exposed-modules: Stackctl.Action + Stackctl.AutoSSO Stackctl.AWS Stackctl.AWS.CloudFormation Stackctl.AWS.Core @@ -108,6 +109,7 @@ library , amazonka-core , amazonka-ec2 , amazonka-lambda + , amazonka-sso , amazonka-sts , base ==4.* , bytestring @@ -130,6 +132,7 @@ library , text , time , transformers + , typed-process , unliftio , unliftio-core , unordered-containers From 47ffd69b93e1e3f9a36d67c9df722306b28e93ec Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Thu, 25 May 2023 15:12:02 -0400 Subject: [PATCH 062/187] Version bump --- CHANGELOG.md | 7 ++++++- package.yaml | 2 +- stackctl.cabal | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eeee9fc..94e3a7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ -## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.4.0.1...main) +## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.4.2.0...main) + +## [v1.4.2.0](https://github.com/freckle/stackctl/compare/v1.4.0.1...v1.4.2.0) + +- Add `stackctl-ls` for listing stacks and their deployed status +- Add `--auto-sso` option for automating `aws sso login` when required ## [v1.4.0.1](https://github.com/freckle/stackctl/compare/v1.4.0.0...v1.4.0.1) diff --git a/package.yaml b/package.yaml index 641b6c6..65450eb 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: stackctl -version: 1.4.0.1 +version: 1.4.2.0 github: freckle/stackctl license: MIT author: Freckle Engineering diff --git a/stackctl.cabal b/stackctl.cabal index dca1673..ebe0009 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -5,7 +5,7 @@ cabal-version: 1.18 -- see: https://github.com/sol/hpack name: stackctl -version: 1.4.0.1 +version: 1.4.2.0 description: Please see homepage: https://github.com/freckle/stackctl#readme bug-reports: https://github.com/freckle/stackctl/issues From 9b29c9dbcc18fa791a11a9ab3ccc571d4155de87 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Thu, 25 May 2023 16:06:07 -0400 Subject: [PATCH 063/187] Setup ronn instead of pandoc in release workflow --- .github/workflows/release.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f091f2e..010f17a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -54,10 +54,11 @@ jobs: steps: - uses: actions/checkout@v3 - uses: freckle/stack-cache-action@v2 - - uses: r-lib/actions/setup-pandoc@v2 + - run: echo "$HOME/.local/share/gem/ruby/3.0.0/bin" >>"$GITHUB_PATH" + - run: gem install --user ronn-ng - if: ${{ runner.os == 'macOS' }} run: brew install coreutils # need GNU install - - run: make install.check PANDOC=pandoc + - run: make install.check - uses: actions/upload-release-asset@v1 id: upload-release-asset env: From 4293e650c60ca9d94a288dcd5bba55328d70848e Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Thu, 25 May 2023 16:06:49 -0400 Subject: [PATCH 064/187] Version bump --- CHANGELOG.md | 6 +++++- package.yaml | 2 +- stackctl.cabal | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94e3a7b..681e8b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ -## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.4.2.0...main) +## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.4.2.1...main) + +## [v1.4.2.1](https://github.com/freckle/stackctl/compare/v1.4.2.0...v1.4.2.1) + +No changes. Bumped to trigger release workflow. ## [v1.4.2.0](https://github.com/freckle/stackctl/compare/v1.4.0.1...v1.4.2.0) diff --git a/package.yaml b/package.yaml index 65450eb..5153b16 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: stackctl -version: 1.4.2.0 +version: 1.4.2.1 github: freckle/stackctl license: MIT author: Freckle Engineering diff --git a/stackctl.cabal b/stackctl.cabal index ebe0009..7be1176 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -5,7 +5,7 @@ cabal-version: 1.18 -- see: https://github.com/sol/hpack name: stackctl -version: 1.4.2.0 +version: 1.4.2.1 description: Please see homepage: https://github.com/freckle/stackctl#readme bug-reports: https://github.com/freckle/stackctl/issues From 4a97f1549e3100df23552a5d67cac8683193646f Mon Sep 17 00:00:00 2001 From: Cristina Grant Date: Mon, 26 Jun 2023 19:29:39 -0400 Subject: [PATCH 065/187] Update README.md (#49) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e095ff6..8b61894 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ This project also contains a Haskell library for doing the same. - Have `~/.local/bin` on your `$PATH` - Have `~/.local/share/man` on your `$MANPATH` (for documentation) - If on OSX, `brew install coreutils` (i.e. have `ginstall` available) +- If on OSX, `brew install jq` ### Scripted From 55744d47b56e2c440feeb5d36aaaea72eeca7d9d Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 14 Jun 2023 09:17:07 -0400 Subject: [PATCH 066/187] Use upstreamed UnliftIO.Exception.Lens --- package.yaml | 3 +-- src/UnliftIO/Exception/Lens.hs | 33 --------------------------------- stack.yaml | 1 + stack.yaml.lock | 7 +++++++ stackctl.cabal | 6 ++---- 5 files changed, 11 insertions(+), 39 deletions(-) delete mode 100644 src/UnliftIO/Exception/Lens.hs diff --git a/package.yaml b/package.yaml index 5153b16..b6c07e0 100644 --- a/package.yaml +++ b/package.yaml @@ -92,8 +92,7 @@ library: - time - transformers - typed-process - - unliftio - - unliftio-core + - unliftio >= 0.2.25.0 # UnliftIO.Exception.Lens - unordered-containers - uuid - yaml diff --git a/src/UnliftIO/Exception/Lens.hs b/src/UnliftIO/Exception/Lens.hs deleted file mode 100644 index da10b83..0000000 --- a/src/UnliftIO/Exception/Lens.hs +++ /dev/null @@ -1,33 +0,0 @@ --- | A copy of "Control.Exception.Lens" on 'MonadUnliftIO' --- --- And only the parts we use in this code-base --- -module UnliftIO.Exception.Lens - ( handling_ - , trying - ) where - -import Prelude - -import Control.Lens (Getting, preview) -import Control.Monad.IO.Unlift (MonadUnliftIO) -import Data.Monoid (First) -import UnliftIO.Exception (SomeException, catchJust, tryJust) - -catching_ - :: MonadUnliftIO m => Getting (First a) SomeException a -> m r -> m r -> m r -catching_ l a b = catchJust (preview l) a (const b) -{-# INLINE catching_ #-} - -handling_ - :: MonadUnliftIO m => Getting (First a) SomeException a -> m r -> m r -> m r -handling_ l = flip (catching_ l) -{-# INLINE handling_ #-} - -trying - :: MonadUnliftIO m - => Getting (First a) SomeException a - -> m r - -> m (Either a r) -trying l = tryJust (preview l) -{-# INLINE trying #-} diff --git a/stack.yaml b/stack.yaml index ccd5b42..f5cf943 100644 --- a/stack.yaml +++ b/stack.yaml @@ -3,6 +3,7 @@ resolver: lts-20.4 extra-deps: - Blammo-1.1.1.1 - cfn-flip-0.1.0.3 + - unliftio-0.2.25.0 - github: brendanhay/amazonka commit: f73a957d05f64863e867cf39d0db260718f0fadd # main, as of SSO support diff --git a/stack.yaml.lock b/stack.yaml.lock index 5085e31..5b3ab93 100644 --- a/stack.yaml.lock +++ b/stack.yaml.lock @@ -18,6 +18,13 @@ packages: size: 3139 original: hackage: cfn-flip-0.1.0.3 +- completed: + hackage: unliftio-0.2.25.0@sha256:d015242554890370bcbc3a575019be691d0edc279736ef97d29412fb9d0c4349,3410 + pantry-tree: + sha256: 08c62f256e740e1a78b175907c26cb06439a1b486ceb8021c5a2e4425ebb6c5b + size: 2494 + original: + hackage: unliftio-0.2.25.0 - completed: name: amazonka pantry-tree: diff --git a/stackctl.cabal b/stackctl.cabal index 7be1176..7e4b7b8 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -1,6 +1,6 @@ cabal-version: 1.18 --- This file has been generated from package.yaml by hpack version 0.35.1. +-- This file has been generated from package.yaml by hpack version 0.35.2. -- -- see: https://github.com/sol/hpack @@ -65,7 +65,6 @@ library Stackctl.TagOption Stackctl.VerboseOption Stackctl.Version - UnliftIO.Exception.Lens other-modules: Paths_stackctl hs-source-dirs: @@ -133,8 +132,7 @@ library , time , transformers , typed-process - , unliftio - , unliftio-core + , unliftio >=0.2.25.0 , unordered-containers , uuid , yaml From 294997d3757f751eda78c28d2313a01cf4d0e6bd Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Fri, 30 Jun 2023 09:00:45 -0400 Subject: [PATCH 067/187] Convert project to Fourmolu --- .restyled.yaml | 4 +- .stylish-haskell.yaml | 25 --- brittany.yaml | 71 -------- fourmolu.yaml | 15 ++ src/Stackctl/AWS/CloudFormation.hs | 169 +++++++++--------- src/Stackctl/AWS/Core.hs | 50 +++--- src/Stackctl/AWS/EC2.hs | 2 +- src/Stackctl/AWS/Lambda.hs | 71 ++++---- src/Stackctl/AWS/Orphans.hs | 70 +++++--- src/Stackctl/AWS/Scope.hs | 46 ++--- src/Stackctl/Action.hs | 27 +-- src/Stackctl/AutoSSO.hs | 18 +- src/Stackctl/CLI.hs | 30 ++-- src/Stackctl/ColorOption.hs | 14 +- src/Stackctl/Colors.hs | 4 +- src/Stackctl/Commands.hs | 78 +++++---- src/Stackctl/Config.hs | 48 +++--- src/Stackctl/Config/RequiredVersion.hs | 18 +- src/Stackctl/DirectoryOption.hs | 31 ++-- src/Stackctl/FilterOption.hs | 34 ++-- src/Stackctl/Options.hs | 41 +++-- src/Stackctl/ParameterOption.hs | 17 +- src/Stackctl/Prelude.hs | 11 +- src/Stackctl/RemovedStack.hs | 2 +- src/Stackctl/Spec/Capture.hs | 110 +++++++----- src/Stackctl/Spec/Cat.hs | 92 +++++----- src/Stackctl/Spec/Changes.hs | 35 ++-- src/Stackctl/Spec/Changes/Format.hs | 88 +++++----- src/Stackctl/Spec/Deploy.hs | 98 ++++++----- src/Stackctl/Spec/Discover.hs | 8 +- src/Stackctl/Spec/Generate.hs | 53 +++--- src/Stackctl/Spec/List.hs | 4 +- src/Stackctl/StackDescription.hs | 16 +- src/Stackctl/StackSpec.hs | 36 ++-- src/Stackctl/StackSpecPath.hs | 45 ++--- src/Stackctl/StackSpecYaml.hs | 23 ++- src/Stackctl/Subcommand.hs | 13 +- src/Stackctl/TagOption.hs | 17 +- src/Stackctl/VerboseOption.hs | 16 +- test/Spec.hs | 2 +- test/Stackctl/AWS/ScopeSpec.hs | 12 +- test/Stackctl/Config/RequiredVersionSpec.hs | 4 +- test/Stackctl/ConfigSpec.hs | 42 ++--- test/Stackctl/FilterOptionSpec.hs | 42 +++-- test/Stackctl/StackDescriptionSpec.hs | 8 +- test/Stackctl/StackSpecSpec.hs | 48 +++--- test/Stackctl/StackSpecYamlSpec.hs | 182 +++++++++++--------- 47 files changed, 987 insertions(+), 903 deletions(-) delete mode 100644 .stylish-haskell.yaml delete mode 100644 brittany.yaml create mode 100644 fourmolu.yaml diff --git a/.restyled.yaml b/.restyled.yaml index dbf806a..f3c387a 100644 --- a/.restyled.yaml +++ b/.restyled.yaml @@ -1,6 +1,8 @@ restylers_version: dev restylers: - - brittany + - fourmolu + - stylish-haskell: + enabled: false - prettier-markdown: enabled: false - whitespace: diff --git a/.stylish-haskell.yaml b/.stylish-haskell.yaml deleted file mode 100644 index b146c9e..0000000 --- a/.stylish-haskell.yaml +++ /dev/null @@ -1,25 +0,0 @@ ---- -steps: - - simple_align: - cases: false - top_level_patterns: false - records: false - - imports: - align: none - list_align: after_alias - pad_module_names: false - long_list_align: new_line_multiline - empty_list_align: right_after - list_padding: 2 - separate_lists: false - space_surround: false - - language_pragmas: - style: vertical - align: false - remove_redundant: false - - trailing_whitespace: {} -columns: 80 -newline: native - -# Infer extensions from .cabal file -cabal: true diff --git a/brittany.yaml b/brittany.yaml deleted file mode 100644 index 368522f..0000000 --- a/brittany.yaml +++ /dev/null @@ -1,71 +0,0 @@ ---- -conf_debug: - dconf_roundtrip_exactprint_only: false - dconf_dump_bridoc_simpl_par: false - dconf_dump_ast_unknown: false - dconf_dump_bridoc_simpl_floating: false - dconf_dump_config: false - dconf_dump_bridoc_raw: false - dconf_dump_bridoc_final: false - dconf_dump_bridoc_simpl_alt: false - dconf_dump_bridoc_simpl_indent: false - dconf_dump_annotations: false - dconf_dump_bridoc_simpl_columns: false - dconf_dump_ast_full: false -conf_forward: - options_ghc: - - -XBangPatterns - - -XDataKinds - - -XDeriveAnyClass - - -XDeriveFoldable - - -XDeriveFunctor - - -XDeriveGeneric - - -XDeriveLift - - -XDeriveTraversable - - -XDerivingStrategies - - -XDerivingVia - - -XFlexibleContexts - - -XFlexibleInstances - - -XGADTs - - -XGeneralizedNewtypeDeriving - - -XLambdaCase - - -XMultiParamTypeClasses - - -XNoImplicitPrelude - - -XNoMonomorphismRestriction - - -XOverloadedStrings - - -XRankNTypes - - -XRecordWildCards - - -XScopedTypeVariables - - -XStandaloneDeriving - - -XTypeApplications - - -XTypeFamilies -conf_errorHandling: - econf_ExactPrintFallback: ExactPrintFallbackModeInline - econf_Werror: false - econf_omit_output_valid_check: false - econf_produceOutputOnErrors: false -conf_preprocessor: - ppconf_CPPMode: CPPModeAbort - ppconf_hackAroundIncludes: false -conf_obfuscate: false -conf_roundtrip_exactprint_only: false -conf_version: 1 -conf_layout: - lconfig_reformatModulePreamble: true - lconfig_altChooser: - tag: AltChooserBoundedSearch - contents: 3 - lconfig_allowSingleLineExportList: false - lconfig_importColumn: 60 - lconfig_hangingTypeSignature: false - lconfig_importAsColumn: 50 - lconfig_alignmentLimit: 1 - lconfig_indentListSpecial: true - lconfig_indentAmount: 2 - lconfig_alignmentBreakOnMultiline: true - lconfig_cols: 80 - lconfig_indentPolicy: IndentPolicyLeft - lconfig_indentWhereSpecial: true - lconfig_columnAlignMode: - tag: ColumnAlignModeDisabled - contents: 0.7 diff --git a/fourmolu.yaml b/fourmolu.yaml new file mode 100644 index 0000000..ef571e8 --- /dev/null +++ b/fourmolu.yaml @@ -0,0 +1,15 @@ +indentation: 2 +column-limit: 80 # ignored until v12 / ghc-9.6 +function-arrows: leading +comma-style: leading # default +import-export-style: leading +indent-wheres: false # default +record-brace-space: true +newlines-between-decls: 1 # default +haddock-style: single-line +let-style: mixed +in-style: left-align +single-constraint-parens: never # ignored until v12 / ghc-9.6 +unicode: never # default +respectful: true # default +fixities: [] # default diff --git a/src/Stackctl/AWS/CloudFormation.hs b/src/Stackctl/AWS/CloudFormation.hs index 71d7094..8a24f5e 100644 --- a/src/Stackctl/AWS/CloudFormation.hs +++ b/src/Stackctl/AWS/CloudFormation.hs @@ -1,23 +1,23 @@ module Stackctl.AWS.CloudFormation - ( Stack(..) + ( Stack (..) , stack_stackName , stackDescription , stackStatusRequiresDeletion - , StackId(..) - , StackName(..) - , StackDescription(..) - , StackStatus(..) - , StackEvent(..) - , ResourceStatus(..) + , StackId (..) + , StackName (..) + , StackDescription (..) + , StackStatus (..) + , StackEvent (..) + , ResourceStatus (..) , stackEvent_eventId , stackEvent_logicalResourceId , stackEvent_resourceStatus , stackEvent_resourceStatusReason , stackEvent_timestamp - , StackTemplate(..) - , StackDeployResult(..) + , StackTemplate (..) + , StackDeployResult (..) , prettyStackDeployResult - , StackDeleteResult(..) + , StackDeleteResult (..) , prettyStackDeleteResult , Parameter , parameter_parameterKey @@ -25,7 +25,7 @@ module Stackctl.AWS.CloudFormation , newParameter , makeParameter , readParameter - , Capability(..) + , Capability (..) , Tag , newTag , tag_key @@ -43,20 +43,20 @@ module Stackctl.AWS.CloudFormation , awsCloudFormationWait , awsCloudFormationGetTemplate - -- * ChangeSets - , ChangeSet(..) + -- * ChangeSets + , ChangeSet (..) , changeSetJSON - , ChangeSetId(..) - , ChangeSetName(..) - , Change(..) - , ResourceChange(..) - , Replacement(..) - , ChangeAction(..) - , ResourceAttribute(..) - , ResourceChangeDetail(..) - , ChangeSource(..) - , ResourceTargetDefinition(..) - , RequiresRecreation(..) + , ChangeSetId (..) + , ChangeSetName (..) + , Change (..) + , ResourceChange (..) + , Replacement (..) + , ChangeAction (..) + , ResourceAttribute (..) + , ResourceChangeDetail (..) + , ChangeSource (..) + , ResourceTargetDefinition (..) + , RequiresRecreation (..) , awsCloudFormationCreateChangeSet , awsCloudFormationExecuteChangeSet , awsCloudFormationDeleteAllChangeSets @@ -80,13 +80,13 @@ import Amazonka.CloudFormation.Waiters import Amazonka.Core ( AsError , ServiceError - , _MatchServiceError - , _ServiceError , hasStatus , serviceCode , serviceMessage + , _MatchServiceError + , _ServiceError ) -import Amazonka.Waiter (Accept(..)) +import Amazonka.Waiter (Accept (..)) import Conduit import Control.Lens ((?~)) import Data.Aeson @@ -127,14 +127,14 @@ data StackDeployResult | StackCreateFailure Bool | StackUpdateSuccess | StackUpdateFailure Bool - deriving stock Show + deriving stock (Show) prettyStackDeployResult :: StackDeployResult -> Text prettyStackDeployResult = \case StackCreateSuccess -> "Created Stack successfully" - StackCreateFailure{} -> "Failed to create Stack" + StackCreateFailure {} -> "Failed to create Stack" StackUpdateSuccess -> "Updated Stack successfully" - StackUpdateFailure{} -> "Failed to update Stack" + StackUpdateFailure {} -> "Failed to update Stack" stackCreateResult :: Accept -> StackDeployResult stackCreateResult = \case @@ -155,7 +155,7 @@ data StackDeleteResult prettyStackDeleteResult :: StackDeleteResult -> Text prettyStackDeleteResult = \case StackDeleteSuccess -> "Deleted Stack successfully" - StackDeleteFailure{} -> "Failed to delete Stack" + StackDeleteFailure {} -> "Failed to delete Stack" stackDeleteResult :: Accept -> StackDeleteResult stackDeleteResult = \case @@ -174,8 +174,7 @@ newChangeSetName = liftIO $ do awsCloudFormationDescribeStack :: (MonadResource m, MonadReader env m, HasAwsEnv env) => StackName -> m Stack awsCloudFormationDescribeStack stackName = do - let - req = newDescribeStacks & describeStacks_stackName ?~ unStackName stackName + let req = newDescribeStacks & describeStacks_stackName ?~ unStackName stackName awsSimple "DescribeStack" req $ \resp -> do stacks <- resp ^. describeStacksResponse_stacks @@ -202,14 +201,14 @@ awsCloudFormationDescribeStackOutputs stackName = do awsCloudFormationDescribeStackEvents :: (MonadResource m, MonadReader env m, HasAwsEnv env) => StackName - -> Maybe Text -- ^ Last-seen Id + -> Maybe Text + -- ^ Last-seen Id -> m [StackEvent] awsCloudFormationDescribeStackEvents stackName mLastId = do - let - req = - newDescribeStackEvents - & describeStackEvents_stackName - ?~ unStackName stackName + let req = + newDescribeStackEvents + & describeStackEvents_stackName + ?~ unStackName stackName runConduit $ awsPaginate req @@ -277,9 +276,10 @@ awsCloudFormationWait => StackName -> m StackDeployResult awsCloudFormationWait stackName = do - either stackCreateResult stackUpdateResult <$> race - (awsAwait newStackCreateComplete req) - (awsAwait newStackUpdateComplete req) + either stackCreateResult stackUpdateResult + <$> race + (awsAwait newStackCreateComplete req) + (awsAwait newStackUpdateComplete req) where req = newDescribeStacks & describeStacks_stackName ?~ unStackName stackName @@ -359,27 +359,27 @@ awsCloudFormationCreateChangeSet -> [Capability] -> [Tag] -> m (Either Text (Maybe ChangeSet)) -awsCloudFormationCreateChangeSet stackName mStackDescription stackTemplate parameters capabilities tags - = fmap (first formatServiceError) +awsCloudFormationCreateChangeSet stackName mStackDescription stackTemplate parameters capabilities tags = + fmap (first formatServiceError) $ trying (_ServiceError . hasStatus 400) $ do - name <- newChangeSetName + name <- newChangeSetName - logDebug $ "Reading Template" :# ["path" .= stackTemplate] - templateBody <- addStackDescription mStackDescription + logDebug $ "Reading Template" :# ["path" .= stackTemplate] + templateBody <- + addStackDescription mStackDescription <$> readFileUtf8 (unStackTemplate stackTemplate) - mStack <- awsCloudFormationDescribeStackMaybe stackName + mStack <- awsCloudFormationDescribeStackMaybe stackName - let - changeSetType = fromMaybe ChangeSetType_CREATE $ do + let changeSetType = fromMaybe ChangeSetType_CREATE $ do stack <- mStack - pure $ if stackIsAbandonedCreate stack - then ChangeSetType_CREATE - else ChangeSetType_UPDATE + pure + $ if stackIsAbandonedCreate stack + then ChangeSetType_CREATE + else ChangeSetType_UPDATE - let - req = + let req = newCreateChangeSet (unStackName stackName) (unChangeSetName name) & (createChangeSet_changeSetType ?~ changeSetType) . (createChangeSet_templateBody ?~ templateBody) @@ -387,17 +387,17 @@ awsCloudFormationCreateChangeSet stackName mStackDescription stackTemplate param . (createChangeSet_capabilities ?~ capabilities) . (createChangeSet_tags ?~ tags) - logInfo - $ "Creating changeset..." - :# ["name" .= name, "type" .= changeSetType] - csId <- awsSimple "CreateChangeSet" req (^. createChangeSetResponse_id) + logInfo + $ "Creating changeset..." + :# ["name" .= name, "type" .= changeSetType] + csId <- awsSimple "CreateChangeSet" req (^. createChangeSetResponse_id) - logDebug "Awaiting CREATE_COMPLETE" - void $ awsAwait newChangeSetCreateComplete $ newDescribeChangeSet csId + logDebug "Awaiting CREATE_COMPLETE" + void $ awsAwait newChangeSetCreateComplete $ newDescribeChangeSet csId - logInfo "Retrieving changeset..." - cs <- awsCloudFormationDescribeChangeSet $ ChangeSetId csId - pure $ cs <$ guard (not $ changeSetFailed cs) + logInfo "Retrieving changeset..." + cs <- awsCloudFormationDescribeChangeSet $ ChangeSetId csId + pure $ cs <$ guard (not $ changeSetFailed cs) awsCloudFormationDescribeChangeSet :: (MonadResource m, MonadReader env m, HasAwsEnv env) @@ -452,15 +452,15 @@ awsCloudFormationDeleteAllChangeSets stackName = do runConduit $ awsPaginate (newListChangeSets $ unStackName stackName) .| concatMapC - (\resp -> fromMaybe [] $ do - ss <- resp ^. listChangeSetsResponse_summaries - pure $ mapMaybe Summary.changeSetId ss - ) + ( \resp -> fromMaybe [] $ do + ss <- resp ^. listChangeSetsResponse_summaries + pure $ mapMaybe Summary.changeSetId ss + ) .| mapM_C - (\csId -> do - logInfo $ "Enqueing delete" :# ["changeSetId" .= csId] - void $ awsSend $ newDeleteChangeSet csId - ) + ( \csId -> do + logInfo $ "Enqueing delete" :# ["changeSetId" .= csId] + void $ awsSend $ newDeleteChangeSet csId + ) -- | Did we abandoned this Stack's first ever ChangeSet? -- @@ -474,16 +474,20 @@ awsCloudFormationDeleteAllChangeSets stackName = do -- Our hueristic for finding these is under review but with no previous -- updates (no lastUpdatedTime), presumably meaning it's still in its /first/ -- review. --- stackIsAbandonedCreate :: Stack -> Bool stackIsAbandonedCreate stack = - stack ^. stack_stackStatus == StackStatus_REVIEW_IN_PROGRESS && isNothing - (stack ^. stack_lastUpdatedTime) + stack + ^. stack_stackStatus + == StackStatus_REVIEW_IN_PROGRESS + && isNothing + (stack ^. stack_lastUpdatedTime) stackStatusRequiresDeletion :: Stack -> Maybe StackStatus -stackStatusRequiresDeletion stack = status - <$ guard (status `elem` requiresDeletionStatuses) - where status = stack ^. stack_stackStatus +stackStatusRequiresDeletion stack = + status + <$ guard (status `elem` requiresDeletionStatuses) + where + status = stack ^. stack_stackStatus requiresDeletionStatuses :: [StackStatus] requiresDeletionStatuses = @@ -501,7 +505,8 @@ _ValidationError = _MatchServiceError defaultService "ValidationError" . hasStatus 400 formatServiceError :: ServiceError -> Text -formatServiceError e = mconcat - [ toText $ e ^. serviceCode - , maybe "" ((": " <>) . toText) $ e ^. serviceMessage - ] +formatServiceError e = + mconcat + [ toText $ e ^. serviceCode + , maybe "" ((": " <>) . toText) $ e ^. serviceMessage + ] diff --git a/src/Stackctl/AWS/Core.hs b/src/Stackctl/AWS/Core.hs index de5bff5..21f2f2b 100644 --- a/src/Stackctl/AWS/Core.hs +++ b/src/Stackctl/AWS/Core.hs @@ -1,6 +1,6 @@ module Stackctl.AWS.Core ( AwsEnv - , HasAwsEnv(..) + , HasAwsEnv (..) , awsEnvDiscover , awsSimple , awsSend @@ -8,23 +8,23 @@ module Stackctl.AWS.Core , awsAwait , awsAssumeRole - -- * Modifiers on 'AwsEnv' + -- * Modifiers on 'AwsEnv' , awsWithin , awsTimeout - -- * 'Amazonka' extensions - , AccountId(..) + -- * 'Amazonka' extensions + , AccountId (..) - -- * 'Amazonka'/'ResourceT' re-exports - , Region(..) - , FromText(..) - , ToText(..) + -- * 'Amazonka'/'ResourceT' re-exports + , Region (..) + , FromText (..) + , ToText (..) , MonadResource ) where import Stackctl.Prelude hiding (timeout) -import Amazonka hiding (LogLevel(..)) +import Amazonka hiding (LogLevel (..)) import qualified Amazonka as AWS import Amazonka.Auth.Keys (fromSession) import Amazonka.STS.AssumeRole @@ -38,7 +38,7 @@ newtype AwsEnv = AwsEnv } unL :: Lens' AwsEnv Env -unL = lens unAwsEnv $ \x y -> x { unAwsEnv = y } +unL = lens unAwsEnv $ \x y -> x {unAwsEnv = y} awsEnvDiscover :: MonadLoggerIO m => m AwsEnv awsEnvDiscover = do @@ -48,19 +48,20 @@ awsEnvDiscover = do configureLogging :: MonadLoggerIO m => Env -> m Env configureLogging env = do loggerIO <- askLoggerIO - pure $ env - { AWS.envLogger = \level msg -> do - loggerIO - defaultLoc -- TODO: there may be a way to get a CallStack/Loc - "Amazonka" - (case level of - AWS.Info -> LevelInfo - AWS.Error -> LevelError - AWS.Debug -> LevelDebug - AWS.Trace -> LevelOther "trace" - ) - (toLogStr msg) - } + pure + $ env + { AWS.envLogger = \level msg -> do + loggerIO + defaultLoc -- TODO: there may be a way to get a CallStack/Loc + "Amazonka" + ( case level of + AWS.Info -> LevelInfo + AWS.Error -> LevelError + AWS.Debug -> LevelDebug + AWS.Trace -> LevelOther "trace" + ) + (toLogStr msg) + } class HasAwsEnv env where awsEnvL :: Lens' env AwsEnv @@ -77,7 +78,8 @@ awsSimple awsSimple name req post = do resp <- awsSend req maybe (throwString err) pure $ post resp - where err = unpack name <> " successful, but processing the response failed" + where + err = unpack name <> " successful, but processing the response failed" awsSend :: (MonadResource m, MonadReader env m, HasAwsEnv env, AWSRequest a) diff --git a/src/Stackctl/AWS/EC2.hs b/src/Stackctl/AWS/EC2.hs index b89d8ba..92528f5 100644 --- a/src/Stackctl/AWS/EC2.hs +++ b/src/Stackctl/AWS/EC2.hs @@ -5,7 +5,7 @@ module Stackctl.AWS.EC2 import Stackctl.Prelude import Amazonka.EC2.DescribeAvailabilityZones -import Amazonka.EC2.Types (AvailabilityZone(..)) +import Amazonka.EC2.Types (AvailabilityZone (..)) import Stackctl.AWS.Core awsEc2DescribeFirstAvailabilityZoneRegionName diff --git a/src/Stackctl/AWS/Lambda.hs b/src/Stackctl/AWS/Lambda.hs index a5566b8..f111736 100644 --- a/src/Stackctl/AWS/Lambda.hs +++ b/src/Stackctl/AWS/Lambda.hs @@ -1,8 +1,8 @@ {-# LANGUAGE MultiWayIf #-} module Stackctl.AWS.Lambda - ( LambdaInvokeResult(..) - , LambdaError(..) + ( LambdaInvokeResult (..) + , LambdaError (..) , logLambdaInvocationResult , isLambdaInvocationSuccess , awsLambdaInvoke @@ -19,36 +19,40 @@ data LambdaInvokeResult = LambdaInvokeSuccess ByteString | LambdaInvokeError LambdaError (Maybe Text) | LambdaInvokeFailure Int (Maybe Text) - deriving stock Show + deriving stock (Show) logLambdaInvocationResult :: MonadLogger m => LambdaInvokeResult -> m () logLambdaInvocationResult = \case LambdaInvokeSuccess bs -> do - let - meta = case decode @Value $ BSL.fromStrict bs of - Nothing -> ["response" .= decodeUtf8 bs] - Just response -> ["response" .= response] + let meta = case decode @Value $ BSL.fromStrict bs of + Nothing -> ["response" .= decodeUtf8 bs] + Just response -> ["response" .= response] logInfo $ "LambdaInvokeSuccess" :# meta LambdaInvokeError LambdaError {..} mFunctionError -> - logError $ (:# []) $ mconcat - [ "LambdaInvokeError" - , "\n errorType: " <> errorType - , "\n errorMessage: " <> errorMessage - , "\n trace: " - , mconcat $ map ("\n " <>) trace - , "\n FunctionError: " <> fromMaybe "none" mFunctionError - ] - LambdaInvokeFailure status mFunctionError -> logError $ (:# []) $ mconcat - [ "LambdaInvokeFailure" - , "\n StatusCode: " <> pack (show status) - , "\n FunctionError: " <> fromMaybe "none" mFunctionError - ] + logError + $ (:# []) + $ mconcat + [ "LambdaInvokeError" + , "\n errorType: " <> errorType + , "\n errorMessage: " <> errorMessage + , "\n trace: " + , mconcat $ map ("\n " <>) trace + , "\n FunctionError: " <> fromMaybe "none" mFunctionError + ] + LambdaInvokeFailure status mFunctionError -> + logError + $ (:# []) + $ mconcat + [ "LambdaInvokeFailure" + , "\n StatusCode: " <> pack (show status) + , "\n FunctionError: " <> fromMaybe "none" mFunctionError + ] isLambdaInvocationSuccess :: LambdaInvokeResult -> Bool isLambdaInvocationSuccess = \case - LambdaInvokeSuccess{} -> True - LambdaInvokeError{} -> False - LambdaInvokeFailure{} -> False + LambdaInvokeSuccess {} -> True + LambdaInvokeError {} -> False + LambdaInvokeFailure {} -> False data LambdaError = LambdaError { errorType :: Text @@ -66,14 +70,20 @@ awsLambdaInvoke , ToJSON a ) => Text - -> a -- ^ Payload + -> a + -- ^ Payload -> m LambdaInvokeResult awsLambdaInvoke name payload = do logDebug $ "Invoking function" :# ["name" .= name] -- Match Lambda's own limit (15 minutes) and add some buffer - resp <- awsTimeout 905 $ awsSend $ newInvoke name $ BSL.toStrict $ encode - payload + resp <- + awsTimeout 905 + $ awsSend + $ newInvoke name + $ BSL.toStrict + $ encode + payload let status = resp ^. invokeResponse_statusCode @@ -89,10 +99,11 @@ awsLambdaInvoke name payload = do , "functionError" .= mFunctionError ] - pure $ if - | statusIsUnsuccessful status -> LambdaInvokeFailure status mFunctionError - | Just e <- mError -> LambdaInvokeError e mFunctionError - | otherwise -> LambdaInvokeSuccess response + pure + $ if + | statusIsUnsuccessful status -> LambdaInvokeFailure status mFunctionError + | Just e <- mError -> LambdaInvokeError e mFunctionError + | otherwise -> LambdaInvokeSuccess response statusIsUnsuccessful :: Int -> Bool statusIsUnsuccessful s = s < 200 || s >= 300 diff --git a/src/Stackctl/AWS/Orphans.hs b/src/Stackctl/AWS/Orphans.hs index e67ec8a..a1867f7 100644 --- a/src/Stackctl/AWS/Orphans.hs +++ b/src/Stackctl/AWS/Orphans.hs @@ -5,9 +5,7 @@ -- -- Orphans so we can get @'ToJSON' 'ChangeSet'@ without hand-writing a massive, -- incomplete, and doomed-to-drift instance ourselves. --- -module Stackctl.AWS.Orphans - () where +module Stackctl.AWS.Orphans () where import Stackctl.Prelude @@ -17,32 +15,54 @@ import Data.Aeson import GHC.Generics (Rep) -- Makes it syntactally easier to do a bunch of these -newtype Generically a = Generically { unGenerically :: a } +newtype Generically a = Generically {unGenerically :: a} instance ( Generic a , GToJSON' Value Zero (Rep a) , GToJSON' Encoding Zero (Rep a) - ) => ToJSON (Generically a) where + ) + => ToJSON (Generically a) + where toJSON = genericToJSON defaultOptions . unGenerically toEncoding = genericToEncoding defaultOptions . unGenerically -deriving via (Generically DescribeChangeSetResponse) - instance ToJSON DescribeChangeSetResponse -deriving via (Generically Tag) - instance ToJSON Tag -deriving via (Generically Parameter) - instance ToJSON Parameter -deriving via (Generically RollbackConfiguration) - instance ToJSON RollbackConfiguration -deriving via (Generically RollbackTrigger) - instance ToJSON RollbackTrigger -deriving via (Generically Change) - instance ToJSON Change -deriving via (Generically ResourceChange) - instance ToJSON ResourceChange -deriving via (Generically ModuleInfo) - instance ToJSON ModuleInfo -deriving via (Generically ResourceChangeDetail) - instance ToJSON ResourceChangeDetail -deriving via (Generically ResourceTargetDefinition) - instance ToJSON ResourceTargetDefinition +deriving via + (Generically DescribeChangeSetResponse) + instance + ToJSON DescribeChangeSetResponse +deriving via + (Generically Tag) + instance + ToJSON Tag +deriving via + (Generically Parameter) + instance + ToJSON Parameter +deriving via + (Generically RollbackConfiguration) + instance + ToJSON RollbackConfiguration +deriving via + (Generically RollbackTrigger) + instance + ToJSON RollbackTrigger +deriving via + (Generically Change) + instance + ToJSON Change +deriving via + (Generically ResourceChange) + instance + ToJSON ResourceChange +deriving via + (Generically ModuleInfo) + instance + ToJSON ModuleInfo +deriving via + (Generically ResourceChangeDetail) + instance + ToJSON ResourceChangeDetail +deriving via + (Generically ResourceTargetDefinition) + instance + ToJSON ResourceTargetDefinition diff --git a/src/Stackctl/AWS/Scope.hs b/src/Stackctl/AWS/Scope.hs index 2ffac7e..6846ba9 100644 --- a/src/Stackctl/AWS/Scope.hs +++ b/src/Stackctl/AWS/Scope.hs @@ -1,8 +1,8 @@ module Stackctl.AWS.Scope - ( AwsScope(..) + ( AwsScope (..) , awsScopeSpecPatterns , awsScopeSpecStackName - , HasAwsScope(..) + , HasAwsScope (..) , fetchAwsScope ) where @@ -20,26 +20,26 @@ data AwsScope = AwsScope , awsRegion :: Region } deriving stock (Eq, Show, Generic) - deriving anyclass ToJSON + deriving anyclass (ToJSON) awsScopeSpecPatterns :: AwsScope -> [Pattern] awsScopeSpecPatterns AwsScope {..} = [ compile - $ "stacks" - unpack (unAccountId awsAccountId) - <> ".*" - unpack (fromRegion awsRegion) - <> "**" - "*" - <.> "yaml" + $ "stacks" + unpack (unAccountId awsAccountId) + <> ".*" + unpack (fromRegion awsRegion) + <> "**" + "*" + <.> "yaml" , compile - $ "stacks" - "*." - <> unpack (unAccountId awsAccountId) - unpack (fromRegion awsRegion) - <> "**" - "*" - <.> "yaml" + $ "stacks" + "*." + <> unpack (unAccountId awsAccountId) + unpack (fromRegion awsRegion) + <> "**" + "*" + <.> "yaml" ] awsScopeSpecStackName :: AwsScope -> FilePath -> Maybe StackName @@ -49,13 +49,13 @@ awsScopeSpecStackName scope path = do -- once we've guarded that the path matches our scope patterns, we can play it -- pretty fast and loose with the "parsing" step pure - $ path -- stacks/account/region/x/y.yaml - & splitPath -- [stacks/, account/, region/, x/, y.yaml] - & drop 3 -- [x, y.yaml] - & joinPath -- x/y.yaml - & dropExtension -- x/y + $ path -- stacks/account/region/x/y.yaml + & splitPath -- [stacks/, account/, region/, x/, y.yaml] + & drop 3 -- [x, y.yaml] + & joinPath -- x/y.yaml + & dropExtension -- x/y & pack - & T.replace "/" "-" -- x-y + & T.replace "/" "-" -- x-y & StackName class HasAwsScope env where diff --git a/src/Stackctl/Action.hs b/src/Stackctl/Action.hs index 9d94494..294db29 100644 --- a/src/Stackctl/Action.hs +++ b/src/Stackctl/Action.hs @@ -11,12 +11,11 @@ -- run: -- InvokeLambdaByStackOutput: OnDeployFunction -- @ --- module Stackctl.Action ( Action , newAction - , ActionOn(..) - , ActionRun(..) + , ActionOn (..) + , ActionRun (..) , runActions ) where @@ -63,18 +62,20 @@ instance FromJSON ActionRun where <|> (InvokeLambdaByName <$> o .: "InvokeLambdaByName") instance ToJSON ActionRun where - toJSON = object . \case - InvokeLambdaByStackOutput name -> ["InvokeLambdaByStackOutput" .= name] - InvokeLambdaByName name -> ["InvokeLambdaByName" .= name] - toEncoding = pairs . \case - InvokeLambdaByStackOutput name -> "InvokeLambdaByStackOutput" .= name - InvokeLambdaByName name -> "InvokeLambdaByName" .= name + toJSON = + object . \case + InvokeLambdaByStackOutput name -> ["InvokeLambdaByStackOutput" .= name] + InvokeLambdaByName name -> ["InvokeLambdaByName" .= name] + toEncoding = + pairs . \case + InvokeLambdaByStackOutput name -> "InvokeLambdaByStackOutput" .= name + InvokeLambdaByName name -> "InvokeLambdaByName" .= name data ActionFailure = NoSuchOutput | InvokeLambdaFailure - deriving stock Show - deriving anyclass Exception + deriving stock (Show) + deriving anyclass (Exception) runActions :: (MonadResource m, MonadLogger m, MonadReader env m, HasAwsEnv env) @@ -86,14 +87,14 @@ runActions stackName on = traverse_ (runAction stackName) . filter (`shouldRunOn` on) shouldRunOn :: Action -> ActionOn -> Bool -shouldRunOn Action { on } on' = on == on' +shouldRunOn Action {on} on' = on == on' runAction :: (MonadResource m, MonadLogger m, MonadReader env m, HasAwsEnv env) => StackName -> Action -> m () -runAction stackName Action { on, run } = do +runAction stackName Action {on, run} = do logInfo $ "Running action" :# ["on" .= on, "run" .= run] case run of diff --git a/src/Stackctl/AutoSSO.hs b/src/Stackctl/AutoSSO.hs index 006def9..3a791f8 100644 --- a/src/Stackctl/AutoSSO.hs +++ b/src/Stackctl/AutoSSO.hs @@ -1,7 +1,7 @@ module Stackctl.AutoSSO ( AutoSSOOption , defaultAutoSSOOption - , HasAutoSSOOption(..) + , HasAutoSSOOption (..) , autoSSOOption , envAutoSSOOption , handleAutoSSO @@ -10,8 +10,8 @@ module Stackctl.AutoSSO import Stackctl.Prelude import Amazonka.SSO (_UnauthorizedException) -import Amazonka.Types (Error, ErrorMessage(..), serviceMessage) -import Data.Semigroup (Last(..)) +import Amazonka.Types (Error, ErrorMessage (..), serviceMessage) +import Data.Semigroup (Last (..)) import qualified Env import Options.Applicative import Stackctl.Prompt @@ -21,7 +21,7 @@ data AutoSSOOption = AutoSSOAlways | AutoSSOAsk | AutoSSONever - deriving Semigroup via Last AutoSSOOption + deriving (Semigroup) via Last AutoSSOOption defaultAutoSSOOption :: AutoSSOOption defaultAutoSSOOption = AutoSSOAsk @@ -38,12 +38,14 @@ class HasAutoSSOOption env where autoSSOOptionL :: Lens' env AutoSSOOption autoSSOOption :: Parser AutoSSOOption -autoSSOOption = option (eitherReader readAutoSSO) - $ mconcat [long "auto-sso", help autoSSOHelp, metavar "WHEN"] +autoSSOOption = + option (eitherReader readAutoSSO) + $ mconcat [long "auto-sso", help autoSSOHelp, metavar "WHEN"] envAutoSSOOption :: Env.Parser Env.Error AutoSSOOption -envAutoSSOOption = Env.var (first Env.UnreadError . readAutoSSO) "AUTO_SSO" - $ Env.help autoSSOHelp +envAutoSSOOption = + Env.var (first Env.UnreadError . readAutoSSO) "AUTO_SSO" + $ Env.help autoSSOHelp autoSSOHelp :: IsString a => a autoSSOHelp = "Automatically run aws-sso-login if necessary?" diff --git a/src/Stackctl/CLI.hs b/src/Stackctl/CLI.hs index 5dec511..1bebca1 100644 --- a/src/Stackctl/CLI.hs +++ b/src/Stackctl/CLI.hs @@ -10,9 +10,9 @@ import Stackctl.Prelude import qualified Blammo.Logging.LogSettings.Env as LoggingEnv import Control.Monad.Catch (MonadCatch) import Control.Monad.Trans.Resource (ResourceT, runResourceT) -import Stackctl.AutoSSO import Stackctl.AWS import Stackctl.AWS.Scope +import Stackctl.AutoSSO import Stackctl.ColorOption import Stackctl.Config import Stackctl.DirectoryOption @@ -28,19 +28,19 @@ data App options = App } optionsL :: Lens' (App options) options -optionsL = lens appOptions $ \x y -> x { appOptions = y } +optionsL = lens appOptions $ \x y -> x {appOptions = y} instance HasLogger (App options) where - loggerL = lens appLogger $ \x y -> x { appLogger = y } + loggerL = lens appLogger $ \x y -> x {appLogger = y} instance HasConfig (App options) where - configL = lens appConfig $ \x y -> x { appConfig = y } + configL = lens appConfig $ \x y -> x {appConfig = y} instance HasAwsScope (App options) where - awsScopeL = lens appAwsScope $ \x y -> x { appAwsScope = y } + awsScopeL = lens appAwsScope $ \x y -> x {appAwsScope = y} instance HasAwsEnv (App options) where - awsEnvL = lens appAwsEnv $ \x y -> x { appAwsEnv = y } + awsEnvL = lens appAwsEnv $ \x y -> x {appAwsEnv = y} instance HasDirectoryOption options => HasDirectoryOption (App options) where directoryOptionL = optionsL . directoryOptionL @@ -87,14 +87,16 @@ runAppT runAppT options f = do envLogSettings <- liftIO - . LoggingEnv.parseWith - . setLogSettingsConcurrency (Just 1) - $ defaultLogSettings - - logger <- newLogger $ adjustLogSettings - (options ^. colorOptionL) - (options ^. verboseOptionL) - envLogSettings + . LoggingEnv.parseWith + . setLogSettingsConcurrency (Just 1) + $ defaultLogSettings + + logger <- + newLogger + $ adjustLogSettings + (options ^. colorOptionL) + (options ^. verboseOptionL) + envLogSettings app <- runResourceT $ runLoggerLoggingT logger $ do aws <- runReaderT (handleAutoSSO options awsEnvDiscover) logger diff --git a/src/Stackctl/ColorOption.hs b/src/Stackctl/ColorOption.hs index 6785ccd..8ac98af 100644 --- a/src/Stackctl/ColorOption.hs +++ b/src/Stackctl/ColorOption.hs @@ -1,23 +1,25 @@ module Stackctl.ColorOption - ( ColorOption(..) - , HasColorOption(..) + ( ColorOption (..) + , HasColorOption (..) , colorOption ) where import Stackctl.Prelude import Blammo.Logging.LogSettings -import Data.Semigroup (Last(..)) +import Data.Semigroup (Last (..)) import Options.Applicative newtype ColorOption = ColorOption { unColorOption :: LogColor } - deriving Semigroup via Last ColorOption + deriving (Semigroup) via Last ColorOption class HasColorOption env where colorOptionL :: Lens' env (Maybe ColorOption) colorOption :: Parser ColorOption -colorOption = option (eitherReader $ fmap ColorOption . readLogColor) $ mconcat - [long "color", help "When to colorize output", metavar "auto|always|never"] +colorOption = + option (eitherReader $ fmap ColorOption . readLogColor) + $ mconcat + [long "color", help "When to colorize output", metavar "auto|always|never"] diff --git a/src/Stackctl/Colors.hs b/src/Stackctl/Colors.hs index 14b50cd..b7283d2 100644 --- a/src/Stackctl/Colors.hs +++ b/src/Stackctl/Colors.hs @@ -1,6 +1,6 @@ -- | Facilities for colorizing output module Stackctl.Colors - ( Colors(..) + ( Colors (..) , getColorsStdout , getColorsLogger , noColors @@ -9,8 +9,8 @@ module Stackctl.Colors import Stackctl.Prelude import Blammo.Logging.Colors -import Blammo.Logging.Logger import Blammo.Logging.LogSettings (shouldColorHandle) +import Blammo.Logging.Logger -- | Return 'Colors' based on options and 'stdout' getColorsStdout :: (MonadIO m, MonadReader env m, HasLogger env) => m Colors diff --git a/src/Stackctl/Commands.hs b/src/Stackctl/Commands.hs index 2a24e17..bad0cb3 100644 --- a/src/Stackctl/Commands.hs +++ b/src/Stackctl/Commands.hs @@ -25,12 +25,13 @@ cat , HasAutoSSOOption options ) => Subcommand options CatOptions -cat = Subcommand - { name = "cat" - , description = "Pretty-print specifications" - , parse = parseCatOptions - , run = runAppSubcommand runCat - } +cat = + Subcommand + { name = "cat" + , description = "Pretty-print specifications" + , parse = parseCatOptions + , run = runAppSubcommand runCat + } capture :: ( HasColorOption options @@ -39,12 +40,13 @@ capture , HasAutoSSOOption options ) => Subcommand options CaptureOptions -capture = Subcommand - { name = "capture" - , description = "Capture deployed Stacks as specifications" - , parse = parseCaptureOptions - , run = runAppSubcommand runCapture - } +capture = + Subcommand + { name = "capture" + , description = "Capture deployed Stacks as specifications" + , parse = parseCaptureOptions + , run = runAppSubcommand runCapture + } changes :: ( HasColorOption options @@ -54,12 +56,13 @@ changes , HasAutoSSOOption options ) => Subcommand options ChangesOptions -changes = Subcommand - { name = "changes" - , description = "Review changes between specification and deployed state" - , parse = parseChangesOptions - , run = runAppSubcommand runChanges - } +changes = + Subcommand + { name = "changes" + , description = "Review changes between specification and deployed state" + , parse = parseChangesOptions + , run = runAppSubcommand runChanges + } deploy :: ( HasColorOption options @@ -69,12 +72,13 @@ deploy , HasAutoSSOOption options ) => Subcommand options DeployOptions -deploy = Subcommand - { name = "deploy" - , description = "Deploy specifications" - , parse = parseDeployOptions - , run = runAppSubcommand runDeploy - } +deploy = + Subcommand + { name = "deploy" + , description = "Deploy specifications" + , parse = parseDeployOptions + , run = runAppSubcommand runDeploy + } list :: ( HasColorOption options @@ -84,17 +88,19 @@ list , HasAutoSSOOption options ) => Subcommand options ListOptions -list = Subcommand - { name = "ls" - , description = "List specifications" - , parse = parseListOptions - , run = runAppSubcommand runList - } +list = + Subcommand + { name = "ls" + , description = "List specifications" + , parse = parseListOptions + , run = runAppSubcommand runList + } version :: Subcommand options () -version = Subcommand - { name = "version" - , description = "Output the version" - , parse = pure () - , run = \() _ -> logVersion - } +version = + Subcommand + { name = "version" + , description = "Output the version" + , parse = pure () + , run = \() _ -> logVersion + } diff --git a/src/Stackctl/Config.hs b/src/Stackctl/Config.hs index aedac47..925a6ea 100644 --- a/src/Stackctl/Config.hs +++ b/src/Stackctl/Config.hs @@ -1,10 +1,10 @@ module Stackctl.Config - ( Config(..) + ( Config (..) , configParameters , configTags , emptyConfig - , HasConfig(..) - , ConfigError(..) + , HasConfig (..) + , ConfigError (..) , loadConfigOrExit , loadConfigFromBytes , applyConfig @@ -25,8 +25,8 @@ data Config = Config { required_version :: Maybe RequiredVersion , defaults :: Maybe Defaults } - deriving stock Generic - deriving anyclass FromJSON + deriving stock (Generic) + deriving anyclass (FromJSON) configParameters :: Config -> Maybe ParametersYaml configParameters = parameters <=< defaults @@ -41,8 +41,8 @@ data Defaults = Defaults { parameters :: Maybe ParametersYaml , tags :: Maybe TagsYaml } - deriving stock Generic - deriving anyclass FromJSON + deriving stock (Generic) + deriving anyclass (FromJSON) class HasConfig env where configL :: Lens' env Config @@ -54,7 +54,7 @@ data ConfigError = ConfigInvalidYaml Yaml.ParseException | ConfigInvalid (NonEmpty Text) | ConfigVersionNotSatisfied RequiredVersion Version - deriving stock Show + deriving stock (Show) configErrorMessage :: ConfigError -> Message configErrorMessage = \case @@ -73,9 +73,10 @@ loadConfigOrExit = either die pure =<< loadConfig exitFailure loadConfig :: MonadIO m => m (Either ConfigError Config) -loadConfig = runExceptT $ getConfigFile >>= \case - Nothing -> pure emptyConfig - Just cf -> loadConfigFrom cf +loadConfig = + runExceptT $ getConfigFile >>= \case + Nothing -> pure emptyConfig + Just cf -> loadConfigFrom cf loadConfigFrom :: (MonadIO m, MonadError ConfigError m) => FilePath -> m Config loadConfigFrom path = loadConfigFromBytes =<< liftIO (readFileBinary path) @@ -91,16 +92,19 @@ loadConfigFromBytes bs = do $ ConfigVersionNotSatisfied rv Paths.version applyConfig :: Config -> StackSpecYaml -> StackSpecYaml -applyConfig config ss@StackSpecYaml {..} = ss - { ssyParameters = configParameters config <> ssyParameters - , ssyTags = configTags config <> ssyTags - } +applyConfig config ss@StackSpecYaml {..} = + ss + { ssyParameters = configParameters config <> ssyParameters + , ssyTags = configTags config <> ssyTags + } getConfigFile :: MonadIO m => m (Maybe FilePath) -getConfigFile = listToMaybe <$> filterM - doesFileExist - [ ".stackctl" "config" <.> "yaml" - , ".stackctl" "config" <.> "yml" - , ".stackctl" <.> "yaml" - , ".stackctl" <.> "yml" - ] +getConfigFile = + listToMaybe + <$> filterM + doesFileExist + [ ".stackctl" "config" <.> "yaml" + , ".stackctl" "config" <.> "yml" + , ".stackctl" <.> "yaml" + , ".stackctl" <.> "yml" + ] diff --git a/src/Stackctl/Config/RequiredVersion.hs b/src/Stackctl/Config/RequiredVersion.hs index 930d7b5..0d83a22 100644 --- a/src/Stackctl/Config/RequiredVersion.hs +++ b/src/Stackctl/Config/RequiredVersion.hs @@ -1,11 +1,11 @@ module Stackctl.Config.RequiredVersion - ( RequiredVersion(..) - , RequiredVersionOp(..) + ( RequiredVersion (..) + , RequiredVersionOp (..) , requiredVersionToText , requiredVersionFromText , isRequiredVersionSatisfied - -- * Exported for testing + -- * Exported for testing , (=~) ) where @@ -39,8 +39,10 @@ instance ToJSON RequiredVersion where requiredVersionToText :: RequiredVersion -> Text requiredVersionToText RequiredVersion {..} = - requiredVersionOpToText requiredVersionOp <> " " <> pack - (showVersion requiredVersionCompareWith) + requiredVersionOpToText requiredVersionOp + <> " " + <> pack + (showVersion requiredVersionCompareWith) requiredVersionFromText :: Text -> Either String RequiredVersion requiredVersionFromText = fromWords . T.words @@ -78,12 +80,14 @@ requiredVersionFromText = fromWords . T.words $ note ("Failed to parse as a version " <> s) $ NE.nonEmpty $ readP_to_S Version.parseVersion s - where s = unpack t + where + s = unpack t isRequiredVersionSatisfied :: RequiredVersion -> Version -> Bool isRequiredVersionSatisfied RequiredVersion {..} = (`requiredVersionCompare` requiredVersionCompareWith) - where requiredVersionCompare = requiredVersionOpCompare requiredVersionOp + where + requiredVersionCompare = requiredVersionOpCompare requiredVersionOp data RequiredVersionOp = RequiredVersionEQ diff --git a/src/Stackctl/DirectoryOption.hs b/src/Stackctl/DirectoryOption.hs index b9d5e3d..a156a84 100644 --- a/src/Stackctl/DirectoryOption.hs +++ b/src/Stackctl/DirectoryOption.hs @@ -1,22 +1,22 @@ module Stackctl.DirectoryOption - ( DirectoryOption(..) + ( DirectoryOption (..) , defaultDirectoryOption - , HasDirectoryOption(..) + , HasDirectoryOption (..) , envDirectoryOption , directoryOption ) where import Stackctl.Prelude -import Data.Semigroup (Last(..)) +import Data.Semigroup (Last (..)) import qualified Env import Options.Applicative newtype DirectoryOption = DirectoryOption { unDirectoryOption :: FilePath } - deriving newtype IsString - deriving Semigroup via Last DirectoryOption + deriving newtype (IsString) + deriving (Semigroup) via Last DirectoryOption defaultDirectoryOption :: DirectoryOption defaultDirectoryOption = "." @@ -28,14 +28,17 @@ instance HasDirectoryOption DirectoryOption where directoryOptionL = id envDirectoryOption :: Env.Parser Env.Error DirectoryOption -envDirectoryOption = Env.var (Env.str <=< Env.nonempty) "DIRECTORY" - $ Env.help "Operate on specifications in this directory" +envDirectoryOption = + Env.var (Env.str <=< Env.nonempty) "DIRECTORY" + $ Env.help "Operate on specifications in this directory" directoryOption :: Parser DirectoryOption -directoryOption = option str $ mconcat - [ short 'd' - , long "directory" - , metavar "PATH" - , help "Operate on specifications in PATH" - , action "directory" - ] +directoryOption = + option str + $ mconcat + [ short 'd' + , long "directory" + , metavar "PATH" + , help "Operate on specifications in PATH" + , action "directory" + ] diff --git a/src/Stackctl/FilterOption.hs b/src/Stackctl/FilterOption.hs index bb2f1e3..985dfbe 100644 --- a/src/Stackctl/FilterOption.hs +++ b/src/Stackctl/FilterOption.hs @@ -1,7 +1,7 @@ module Stackctl.FilterOption ( FilterOption , defaultFilterOption - , HasFilterOption(..) + , HasFilterOption (..) , envFilterOption , filterOption , filterOptionFromPaths @@ -13,11 +13,11 @@ module Stackctl.FilterOption import Stackctl.Prelude import qualified Data.List.NonEmpty as NE -import Data.Semigroup (Last(..)) +import Data.Semigroup (Last (..)) import qualified Data.Text as T import qualified Env import Options.Applicative -import Stackctl.AWS.CloudFormation (StackName(..)) +import Stackctl.AWS.CloudFormation (StackName (..)) import Stackctl.StackSpec import System.FilePath (hasExtension) import System.FilePath.Glob @@ -25,7 +25,7 @@ import System.FilePath.Glob newtype FilterOption = FilterOption { unFilterOption :: NonEmpty Pattern } - deriving Semigroup via Last FilterOption + deriving (Semigroup) via Last FilterOption instance ToJSON FilterOption where toJSON = toJSON . showFilterOption @@ -48,11 +48,13 @@ envFilterOption items = var "FILTERS" <|> var "FILTER" <> " by patterns" filterOption :: String -> Parser FilterOption -filterOption items = option (eitherReader readFilterOption) $ mconcat - [ long "filter" - , metavar "PATTERN[,PATTERN]" - , help $ "Filter " <> items <> " to match PATTERN(s)" - ] +filterOption items = + option (eitherReader readFilterOption) + $ mconcat + [ long "filter" + , metavar "PATTERN[,PATTERN]" + , help $ "Filter " <> items <> " to match PATTERN(s)" + ] filterOptionFromPaths :: NonEmpty FilePath -> FilterOption filterOptionFromPaths = FilterOption . fmap compile @@ -83,7 +85,8 @@ expandPatterns t = map compile $ s : expanded readFilterOption :: String -> Either String FilterOption readFilterOption = note err . filterOptionFromText . pack - where err = "Must be non-empty, comma-separated list of non-empty patterns" + where + err = "Must be non-empty, comma-separated list of non-empty patterns" showFilterOption :: FilterOption -> String showFilterOption = @@ -104,8 +107,9 @@ filterStackSpecs fo = filter $ \spec -> any (`matchStackSpec` spec) $ unFilterOption fo matchStackSpec :: Pattern -> StackSpec -> Bool -matchStackSpec p spec = or - [ match p $ unpack $ unStackName $ stackSpecStackName spec - , match p $ stackSpecStackFile spec - , match p $ stackSpecTemplateFile spec - ] +matchStackSpec p spec = + or + [ match p $ unpack $ unStackName $ stackSpecStackName spec + , match p $ stackSpecStackFile spec + , match p $ stackSpecTemplateFile spec + ] diff --git a/src/Stackctl/Options.hs b/src/Stackctl/Options.hs index f9b279b..ba3f927 100644 --- a/src/Stackctl/Options.hs +++ b/src/Stackctl/Options.hs @@ -22,17 +22,17 @@ data Options = Options , oVerbose :: Verbosity , oAutoSSO :: Maybe AutoSSOOption } - deriving stock Generic - deriving Semigroup via GenericSemigroupMonoid Options + deriving stock (Generic) + deriving (Semigroup) via GenericSemigroupMonoid Options directoryL :: Lens' Options (Maybe DirectoryOption) -directoryL = lens oDirectory $ \x y -> x { oDirectory = y } +directoryL = lens oDirectory $ \x y -> x {oDirectory = y} filterL :: Lens' Options (Maybe FilterOption) -filterL = lens oFilter $ \x y -> x { oFilter = y } +filterL = lens oFilter $ \x y -> x {oFilter = y} autoSSOL :: Lens' Options (Maybe AutoSSOOption) -autoSSOL = lens oAutoSSO $ \x y -> x { oAutoSSO = y } +autoSSOL = lens oAutoSSO $ \x y -> x {oAutoSSO = y} instance HasDirectoryOption Options where directoryOptionL = directoryL . maybeLens defaultDirectoryOption @@ -41,10 +41,10 @@ instance HasFilterOption Options where filterOptionL = filterL . maybeLens defaultFilterOption instance HasColorOption Options where - colorOptionL = lens oColor $ \x y -> x { oColor = y } + colorOptionL = lens oColor $ \x y -> x {oColor = y} instance HasVerboseOption Options where - verboseOptionL = lens oVerbose $ \x y -> x { oVerbose = y } + verboseOptionL = lens oVerbose $ \x y -> x {oVerbose = y} instance HasAutoSSOOption Options where autoSSOOptionL = autoSSOL . maybeLens defaultAutoSSOOption @@ -52,19 +52,22 @@ instance HasAutoSSOOption Options where -- brittany-disable-next-binding envParser :: Env.Parser Env.Error Options -envParser = Env.prefixed "STACKCTL_" $ Options - <$> optional envDirectoryOption - <*> optional (envFilterOption "specifications") - <*> pure mempty -- use LOG_COLOR - <*> pure mempty -- use LOG_LEVEL - <*> optional envAutoSSOOption +envParser = + Env.prefixed "STACKCTL_" + $ Options + <$> optional envDirectoryOption + <*> optional (envFilterOption "specifications") + <*> pure mempty -- use LOG_COLOR + <*> pure mempty -- use LOG_LEVEL + <*> optional envAutoSSOOption -- brittany-disable-next-binding optionsParser :: Parser Options -optionsParser = Options - <$> optional directoryOption - <*> optional (filterOption "specifications") - <*> optional colorOption - <*> verboseOption - <*> optional autoSSOOption +optionsParser = + Options + <$> optional directoryOption + <*> optional (filterOption "specifications") + <*> optional colorOption + <*> verboseOption + <*> optional autoSSOOption diff --git a/src/Stackctl/ParameterOption.hs b/src/Stackctl/ParameterOption.hs index e579c9b..ea53730 100644 --- a/src/Stackctl/ParameterOption.hs +++ b/src/Stackctl/ParameterOption.hs @@ -9,12 +9,14 @@ import Options.Applicative import Stackctl.AWS.CloudFormation (Parameter, makeParameter) parameterOption :: Parser Parameter -parameterOption = option (eitherReader readParameter) $ mconcat - [ short 'p' - , long "parameter" - , metavar "KEY=[VALUE]" - , help "Override the given Parameter for this operation" - ] +parameterOption = + option (eitherReader readParameter) + $ mconcat + [ short 'p' + , long "parameter" + , metavar "KEY=[VALUE]" + , help "Override the given Parameter for this operation" + ] readParameter :: String -> Either String Parameter readParameter s = case T.breakOn "=" t of @@ -22,4 +24,5 @@ readParameter s = case T.breakOn "=" t of (k, _) | T.null k -> Left $ "Empty key (" <> s <> ")" (k, "=") -> Right $ makeParameter k $ Just "" (k, v) -> Right $ makeParameter k $ Just $ T.drop 1 v - where t = pack s + where + t = pack s diff --git a/src/Stackctl/Prelude.hs b/src/Stackctl/Prelude.hs index 307b834..614ec85 100644 --- a/src/Stackctl/Prelude.hs +++ b/src/Stackctl/Prelude.hs @@ -5,7 +5,7 @@ module Stackctl.Prelude ) where import RIO as X hiding - ( LogLevel(..) + ( LogLevel (..) , LogSource , logDebug , logDebugS @@ -21,10 +21,15 @@ import RIO as X hiding import Blammo.Logging as X import Control.Error.Util as X (hush, note) -import Data.Aeson as X (ToJSON(..), object) +import Data.Aeson as X (ToJSON (..), object) import Data.Text as X (pack, unpack) import System.FilePath as X - (dropExtension, takeBaseName, takeDirectory, (<.>), ()) + ( dropExtension + , takeBaseName + , takeDirectory + , (<.>) + , () + ) import UnliftIO.Directory as X (withCurrentDirectory) {-# ANN module ("HLint: ignore Avoid restricted alias" :: String) #-} diff --git a/src/Stackctl/RemovedStack.hs b/src/Stackctl/RemovedStack.hs index 77b2086..3c3ef14 100644 --- a/src/Stackctl/RemovedStack.hs +++ b/src/Stackctl/RemovedStack.hs @@ -5,7 +5,7 @@ module Stackctl.RemovedStack import Stackctl.Prelude import Control.Error.Util (hoistMaybe) -import Control.Monad.Trans.Maybe (MaybeT(..), runMaybeT) +import Control.Monad.Trans.Maybe (MaybeT (..), runMaybeT) import Stackctl.AWS.CloudFormation import Stackctl.AWS.Core import Stackctl.AWS.Scope diff --git a/src/Stackctl/Spec/Capture.hs b/src/Stackctl/Spec/Capture.hs index 557d103..0771783 100644 --- a/src/Stackctl/Spec/Capture.hs +++ b/src/Stackctl/Spec/Capture.hs @@ -1,5 +1,5 @@ module Stackctl.Spec.Capture - ( CaptureOptions(..) + ( CaptureOptions (..) , parseCaptureOptions , runCapture ) where @@ -27,37 +27,51 @@ data CaptureOptions = CaptureOptions -- brittany-disable-next-binding parseCaptureOptions :: Parser CaptureOptions -parseCaptureOptions = CaptureOptions - <$> optional (strOption - ( short 'n' - <> long "account-name" - <> metavar "NAME" - <> help "Account name to use in generated files" - )) - <*> optional (strOption - ( short 't' - <> long "template-path" - <> metavar "PATH" - <> help "Write Template to PATH. Default is based on STACK" - )) - <*> optional (strOption - ( short 'p' - <> long "path" - <> metavar "PATH" - <> help "Write specification to PATH. Default is based on STACK" - )) - <*> optional (some (StackName <$> strOption - ( long "depend" - <> metavar "STACK" - <> help "Add a dependency on STACK" - ))) - <*> flag TemplateFormatYaml TemplateFormatJson - ( long "no-flip" - <> help "Don't flip JSON templates to Yaml" +parseCaptureOptions = + CaptureOptions + <$> optional + ( strOption + ( short 'n' + <> long "account-name" + <> metavar "NAME" + <> help "Account name to use in generated files" + ) + ) + <*> optional + ( strOption + ( short 't' + <> long "template-path" + <> metavar "PATH" + <> help "Write Template to PATH. Default is based on STACK" + ) + ) + <*> optional + ( strOption + ( short 'p' + <> long "path" + <> metavar "PATH" + <> help "Write specification to PATH. Default is based on STACK" + ) + ) + <*> optional + ( some + ( StackName + <$> strOption + ( long "depend" + <> metavar "STACK" + <> help "Add a dependency on STACK" + ) + ) + ) + <*> flag + TemplateFormatYaml + TemplateFormatJson + ( long "no-flip" + <> help "Don't flip JSON templates to Yaml" ) <*> strArgument - ( metavar "STACK" - <> help "Name of deployed Stack to capture" + ( metavar "STACK" + <> help "Name of deployed Stack to capture" ) runCapture @@ -76,28 +90,31 @@ runCapture runCapture CaptureOptions {..} = do let setScopeName scope = - maybe scope (\name -> scope { awsAccountName = name }) scoAccountName + maybe scope (\name -> scope {awsAccountName = name}) scoAccountName generate' stack template path templatePath = do let stackName = StackName $ stack ^. stack_stackName templateBody = templateBodyFromValue template - void $ local (awsScopeL %~ setScopeName) $ generate Generate - { gDescription = stackDescription stack - , gDepends = scoDepends - , gActions = Nothing - , gParameters = parameters stack - , gCapabilities = capabilities stack - , gTags = tags stack - , gSpec = case path of - Nothing -> GenerateSpec stackName - Just sp -> GenerateSpecTo stackName sp - , gTemplate = case templatePath of - Nothing -> GenerateTemplate templateBody scoTemplateFormat - Just tp -> GenerateTemplateTo templateBody tp - , gOverwrite = False - } + void + $ local (awsScopeL %~ setScopeName) + $ generate + Generate + { gDescription = stackDescription stack + , gDepends = scoDepends + , gActions = Nothing + , gParameters = parameters stack + , gCapabilities = capabilities stack + , gTags = tags stack + , gSpec = case path of + Nothing -> GenerateSpec stackName + Just sp -> GenerateSpecTo stackName sp + , gTemplate = case templatePath of + Nothing -> GenerateTemplate templateBody scoTemplateFormat + Just tp -> GenerateTemplateTo templateBody tp + , gOverwrite = False + } results <- awsCloudFormationGetStackNamesMatching scoStackName @@ -108,7 +125,6 @@ runCapture CaptureOptions {..} = do <> pack (decompile scoStackName) :# [] exitFailure - [stackName] -> do stack <- awsCloudFormationDescribeStack stackName template <- awsCloudFormationGetTemplate stackName diff --git a/src/Stackctl/Spec/Cat.hs b/src/Stackctl/Spec/Cat.hs index 4a1235d..b438326 100644 --- a/src/Stackctl/Spec/Cat.hs +++ b/src/Stackctl/Spec/Cat.hs @@ -1,5 +1,5 @@ module Stackctl.Spec.Cat - ( CatOptions(..) + ( CatOptions (..) , parseCatOptions , runCat ) where @@ -21,7 +21,7 @@ import Stackctl.AWS import Stackctl.AWS.Scope import Stackctl.Colors import Stackctl.Config (HasConfig) -import Stackctl.DirectoryOption (HasDirectoryOption(..), unDirectoryOption) +import Stackctl.DirectoryOption (HasDirectoryOption (..), unDirectoryOption) import Stackctl.FilterOption (HasFilterOption) import Stackctl.Spec.Discover import Stackctl.StackSpec @@ -37,20 +37,21 @@ data CatOptions = CatOptions -- 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" - ) +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 @@ -114,21 +115,23 @@ specTree = map (second groupRegion) . groupAccount groupRegion = groupTo (stackSpecPathRegion . stackSpecSpecPath) groupAccount :: [StackSpec] -> [((AccountId, Text), [StackSpec])] - groupAccount = groupTo - ((stackSpecPathAccountId &&& stackSpecPathAccountName) . stackSpecSpecPath) + 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 - ] +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 @@ -136,10 +139,10 @@ prettyPrintStackSpecYaml Colors {..} name StackSpecYaml {..} = concat pure $ [cyan label <> ":"] <> map - (\(k, mV) -> - " " <> cyan k <> ":" <> maybe "" (\v -> " " <> green v) mV - ) - kvs + ( \(k, mV) -> + " " <> cyan k <> ":" <> maybe "" (\v -> " " <> green v) mV + ) + kvs ppList :: Text -> (a -> [Text]) -> Maybe a -> [Text] ppList label f = maybe [] (((cyan label <> ":") :) . f) @@ -152,9 +155,13 @@ 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 +parameterYamlKV py = + (,) + <$> (p ^. parameter_parameterKey) + <*> pure + (p ^. parameter_parameterValue) + where + p = unParameterYaml py tagsYamlKVs :: TagsYaml -> [(Text, Maybe Text)] tagsYamlKVs = map (tagKV . unTagYaml) . unTagsYaml @@ -163,12 +170,13 @@ 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" - ] +prettyPrintTemplate Colors {..} val = + concat + [ displayTextProperty "Description" + , displayObjectProperty "Parameters" + , displayObjectProperty "Resources" + , displayObjectProperty "Outputs" + ] where displayTextProperty :: Text -> [Text] displayTextProperty = displayPropertyWith @@ -184,7 +192,8 @@ prettyPrintTemplate Colors {..} val = concat 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 + where + displayValue = val ^? key (Key.fromText k) . _JSON . to f putBoxed :: MonadIO m => Int -> [Text] -> m () putBoxed n xs = do @@ -194,4 +203,5 @@ putBoxed n xs = do put :: MonadIO m => Int -> Text -> m () put n = liftIO . T.putStrLn . (indent <>) - where indent = mconcat $ replicate n " " + where + indent = mconcat $ replicate n " " diff --git a/src/Stackctl/Spec/Changes.hs b/src/Stackctl/Spec/Changes.hs index 19f8ff6..7ff6ebe 100644 --- a/src/Stackctl/Spec/Changes.hs +++ b/src/Stackctl/Spec/Changes.hs @@ -1,5 +1,5 @@ module Stackctl.Spec.Changes - ( ChangesOptions(..) + ( ChangesOptions (..) , parseChangesOptions , runChanges ) where @@ -34,16 +34,20 @@ data ChangesOptions = ChangesOptions -- brittany-disable-next-binding parseChangesOptions :: Parser ChangesOptions -parseChangesOptions = ChangesOptions - <$> formatOption - <*> omitFullOption - <*> many parameterOption - <*> many tagOption - <*> optional (argument str - ( metavar "PATH" - <> help "Write changes summary to PATH" - <> action "file" - )) +parseChangesOptions = + ChangesOptions + <$> formatOption + <*> omitFullOption + <*> many parameterOption + <*> many tagOption + <*> optional + ( argument + str + ( metavar "PATH" + <> help "Write changes summary to PATH" + <> action "file" + ) + ) runChanges :: ( MonadMask m @@ -66,12 +70,11 @@ runChanges ChangesOptions {..} = do colors <- case scoOutput of Nothing -> getColorsLogger - Just{} -> pure noColors + Just {} -> pure noColors - let - write formatted = case scoOutput of - Nothing -> pushLoggerLn formatted - Just p -> liftIO $ T.appendFile p $ formatted <> "\n" + let write formatted = case scoOutput of + Nothing -> pushLoggerLn formatted + Just p -> liftIO $ T.appendFile p $ formatted <> "\n" specs <- discoverSpecs diff --git a/src/Stackctl/Spec/Changes/Format.hs b/src/Stackctl/Spec/Changes/Format.hs index 3447c8a..0ba7b16 100644 --- a/src/Stackctl/Spec/Changes/Format.hs +++ b/src/Stackctl/Spec/Changes/Format.hs @@ -1,7 +1,7 @@ module Stackctl.Spec.Changes.Format - ( Format(..) + ( Format (..) , formatOption - , OmitFull(..) + , OmitFull (..) , omitFullOption , formatChangeSet , formatRemovedStack @@ -25,13 +25,15 @@ data OmitFull | IncludeFull formatOption :: Parser Format -formatOption = option (eitherReader readFormat) $ mconcat - [ short 'f' - , long "format" - , help "Format to output changes in" - , value FormatTTY - , showDefaultWith showFormat - ] +formatOption = + option (eitherReader readFormat) + $ mconcat + [ short 'f' + , long "format" + , help "Format to output changes in" + , value FormatTTY + , showDefaultWith showFormat + ] readFormat :: String -> Either String Format readFormat = \case @@ -47,10 +49,13 @@ showFormat = \case -- brittany-disable-next-binding omitFullOption :: Parser OmitFull -omitFullOption = flag IncludeFull OmitFull - ( long "no-include-full" - <> help "Don't include full ChangeSet JSON details" - ) +omitFullOption = + flag + IncludeFull + OmitFull + ( long "no-include-full" + <> help "Don't include full ChangeSet JSON details" + ) formatChangeSet :: Colors -> OmitFull -> Text -> Format -> Maybe ChangeSet -> Text @@ -62,16 +67,18 @@ formatRemovedStack :: Colors -> Format -> Stack -> Text formatRemovedStack Colors {..} format stack = case format of FormatTTY -> red "DELETE" <> " stack " <> cyan name FormatPullRequest -> ":x: This PR will **delete** the stack `" <> name <> "`" - where name = stack ^. stack_stackName + where + name = stack ^. stack_stackName formatTTY :: Colors -> Text -> Maybe ChangeSet -> Text formatTTY colors@Colors {..} name mChangeSet = case (mChangeSet, rChanges) of (Nothing, _) -> "No changes for " <> name (_, Nothing) -> "Metadata only changes (e.g. Tags or Outputs)" (_, Just rcs) -> - ("\n" <>) $ (<> "\n") $ mconcat $ ("Changes for " <> cyan name <> ":") : map - (("\n " <>) . formatResourceChange) - (NE.toList rcs) + ("\n" <>) $ (<> "\n") $ mconcat $ ("Changes for " <> cyan name <> ":") + : map + (("\n " <>) . formatResourceChange) + (NE.toList rcs) where rChanges = do cs <- mChangeSet @@ -143,31 +150,32 @@ commentBody omitFull cs rcs = ] <> map commentTableRow (NE.toList rcs) <> case omitFull of - OmitFull -> [] - IncludeFull -> - [ "\n" - , "\n
" - , "\nFull changes" - , "\n" - , "\n```json" - , "\n" <> changeSetJSON cs - , "\n```" - , "\n" - , "\n
" - ] + OmitFull -> [] + IncludeFull -> + [ "\n" + , "\n
" + , "\nFull changes" + , "\n" + , "\n```json" + , "\n" <> changeSetJSON cs + , "\n```" + , "\n" + , "\n
" + ] commentTableRow :: ResourceChange -> Text -commentTableRow ResourceChange' {..} = mconcat - [ "\n" - , "| " <> maybe "" toText action <> " " - , "| " <> maybe "" toText logicalResourceId <> " " - , "| " <> maybe "" toText physicalResourceId <> " " - , "| " <> maybe "" toText resourceType <> " " - , "| " <> maybe "" toText replacement <> " " - , "| " <> maybe "" (T.intercalate ", " . map toText) scope <> " " - , "| " <> maybe "" (mdList . mapMaybe (formatDetail noColors)) details <> " " - , "|" - ] +commentTableRow ResourceChange' {..} = + mconcat + [ "\n" + , "| " <> maybe "" toText action <> " " + , "| " <> maybe "" toText logicalResourceId <> " " + , "| " <> maybe "" toText physicalResourceId <> " " + , "| " <> maybe "" toText resourceType <> " " + , "| " <> maybe "" toText replacement <> " " + , "| " <> maybe "" (T.intercalate ", " . map toText) scope <> " " + , "| " <> maybe "" (mdList . mapMaybe (formatDetail noColors)) details <> " " + , "|" + ] mdList :: [Text] -> Text mdList xs = diff --git a/src/Stackctl/Spec/Deploy.hs b/src/Stackctl/Spec/Deploy.hs index 1a055e7..f3518d6 100644 --- a/src/Stackctl/Spec/Deploy.hs +++ b/src/Stackctl/Spec/Deploy.hs @@ -1,6 +1,6 @@ module Stackctl.Spec.Deploy - ( DeployOptions(..) - , DeployConfirmation(..) + ( DeployOptions (..) + , DeployConfirmation (..) , parseDeployOptions , runDeploy ) where @@ -11,9 +11,9 @@ import Blammo.Logging.Logger (pushLoggerLn) import qualified Data.Text as T import Data.Time (defaultTimeLocale, formatTime, utcToLocalZonedTime) import Options.Applicative -import Stackctl.Action import Stackctl.AWS hiding (action) import Stackctl.AWS.Scope +import Stackctl.Action import Stackctl.Colors import Stackctl.Config (HasConfig) import Stackctl.DirectoryOption (HasDirectoryOption) @@ -39,27 +39,34 @@ data DeployOptions = DeployOptions -- brittany-disable-next-binding parseDeployOptions :: Parser DeployOptions -parseDeployOptions = DeployOptions - <$> many parameterOption - <*> many tagOption - <*> optional (strOption - ( long "save-change-sets" - <> metavar "DIRECTORY" - <> help "Save executed changesets to DIRECTORY" - <> action "directory" - )) - <*> flag DeployWithConfirmation DeployWithoutConfirmation - ( long "no-confirm" - <> help "Don't confirm changes before executing" - ) - <*> (not <$> switch - ( long "no-remove" - <> help "Don't delete removed Stacks" - )) - <*> switch - ( long "clean" - <> help "Remove all changesets from Stack after deploy" - ) +parseDeployOptions = + DeployOptions + <$> many parameterOption + <*> many tagOption + <*> optional + ( strOption + ( long "save-change-sets" + <> metavar "DIRECTORY" + <> help "Save executed changesets to DIRECTORY" + <> action "directory" + ) + ) + <*> flag + DeployWithConfirmation + DeployWithoutConfirmation + ( long "no-confirm" + <> help "Don't confirm changes before executing" + ) + <*> ( not + <$> switch + ( long "no-remove" + <> help "Don't delete removed Stacks" + ) + ) + <*> switch + ( long "clean" + <> help "Remove all changesets from Stack after deploy" + ) runDeploy :: ( MonadMask m @@ -131,12 +138,13 @@ deleteRemovedStack confirmation stack = do DeployWithoutConfirmation -> pure () deleteStack stackName - where stackName = StackName $ stack ^. stack_stackName + where + stackName = StackName $ stack ^. stack_stackName data DeployConfirmation = DeployWithConfirmation | DeployWithoutConfirmation - deriving stock Eq + deriving stock (Eq) checkIfStackRequiresDeletion :: ( MonadUnliftIO m @@ -156,7 +164,7 @@ checkIfStackRequiresDeletion confirmation stackName = do logWarn $ "Stack must be deleted before proceeding" :# ["status" .= status] when (status == StackStatus_ROLLBACK_FAILED) $ logWarn - "Stack is in ROLLBACK_FAILED. This may require elevated permissions for the delete to succeed" + "Stack is in ROLLBACK_FAILED. This may require elevated permissions for the delete to succeed" case confirmation of DeployWithConfirmation -> promptContinue @@ -176,7 +184,7 @@ deleteStack stackName = do case result of StackDeleteSuccess -> logInfo $ prettyStackDeleteResult result :# [] - StackDeleteFailure{} -> logWarn $ prettyStackDeleteResult result :# [] + StackDeleteFailure {} -> logWarn $ prettyStackDeleteResult result :# [] deployChangeSet :: ( MonadUnliftIO m @@ -218,9 +226,9 @@ deployChangeSet confirmation changeSet = do case result of StackCreateSuccess -> onSuccess - StackCreateFailure{} -> onFailure + StackCreateFailure {} -> onFailure StackUpdateSuccess -> onSuccess - StackUpdateFailure{} -> onFailure + StackUpdateFailure {} -> onFailure where stackName = csStackName changeSet changeSetId = csChangeSetId changeSet @@ -233,7 +241,8 @@ tailStackEventsSince , HasAwsEnv env ) => StackName - -> Maybe Text -- ^ StackEventId + -> Maybe Text + -- ^ StackEventId -> m a tailStackEventsSince stackName mLastId = do colors <- getColorsLogger @@ -251,18 +260,21 @@ tailStackEventsSince stackName mLastId = do formatStackEvent :: MonadIO m => Colors -> StackEvent -> m Text formatStackEvent Colors {..} e = do timestamp <- - liftIO $ formatTime defaultTimeLocale "%F %T %Z" <$> utcToLocalZonedTime - (e ^. stackEvent_timestamp) - - pure $ mconcat - [ fromString timestamp - , " | " - , maybe "" colorStatus $ e ^. stackEvent_resourceStatus - , maybe "" (magenta . (" " <>)) $ e ^. stackEvent_logicalResourceId - , maybe "" ((\x -> " (" <> x <> ")") . T.strip) - $ e - ^. stackEvent_resourceStatusReason - ] + liftIO + $ formatTime defaultTimeLocale "%F %T %Z" + <$> utcToLocalZonedTime + (e ^. stackEvent_timestamp) + + pure + $ mconcat + [ fromString timestamp + , " | " + , maybe "" colorStatus $ e ^. stackEvent_resourceStatus + , maybe "" (magenta . (" " <>)) $ e ^. stackEvent_logicalResourceId + , maybe "" ((\x -> " (" <> x <> ")") . T.strip) + $ e + ^. stackEvent_resourceStatusReason + ] where colorStatus = \case ResourceStatus' x diff --git a/src/Stackctl/Spec/Discover.hs b/src/Stackctl/Spec/Discover.hs index 1793e99..6153ee8 100644 --- a/src/Stackctl/Spec/Discover.hs +++ b/src/Stackctl/Spec/Discover.hs @@ -10,8 +10,8 @@ import qualified Data.List.NonEmpty as NE import Stackctl.AWS import Stackctl.AWS.Scope import Stackctl.Config (HasConfig) -import Stackctl.DirectoryOption (HasDirectoryOption(..), unDirectoryOption) -import Stackctl.FilterOption (HasFilterOption(..), filterStackSpecs) +import Stackctl.DirectoryOption (HasDirectoryOption (..), unDirectoryOption) +import Stackctl.FilterOption (HasFilterOption (..), filterStackSpecs) import Stackctl.StackSpec import Stackctl.StackSpecPath import System.FilePath (isPathSeparator) @@ -51,8 +51,8 @@ discoverSpecs = do specs <- sortStackSpecs - . filterStackSpecs filterOption - <$> traverse (readStackSpec dir) specPaths + . filterStackSpecs filterOption + <$> traverse (readStackSpec dir) specPaths when (null specs) $ logWarn "No specs found" specs <$ logDebug ("Discovered specs" :# ["matched" .= length specs]) diff --git a/src/Stackctl/Spec/Generate.hs b/src/Stackctl/Spec/Generate.hs index c316954..c3b22db 100644 --- a/src/Stackctl/Spec/Generate.hs +++ b/src/Stackctl/Spec/Generate.hs @@ -1,16 +1,16 @@ module Stackctl.Spec.Generate - ( Generate(..) - , GenerateSpec(..) - , GenerateTemplate(..) + ( Generate (..) + , GenerateSpec (..) + , GenerateTemplate (..) , generate - , TemplateFormat(..) + , TemplateFormat (..) ) where import Stackctl.Prelude -import Stackctl.Action import Stackctl.AWS import Stackctl.AWS.Scope +import Stackctl.Action import Stackctl.Config (HasConfig) import Stackctl.DirectoryOption import Stackctl.Spec.Discover (buildSpecPath) @@ -31,18 +31,18 @@ data Generate = Generate } data GenerateSpec - = GenerateSpec StackName - -- ^ Generate at an inferred name - | GenerateSpecTo StackName FilePath - -- ^ Generate to a given path + = -- | Generate at an inferred name + GenerateSpec StackName + | -- | Generate to a given path + GenerateSpecTo StackName FilePath data GenerateTemplate - = GenerateTemplate TemplateBody TemplateFormat - -- ^ Generate at an inferred name - | GenerateTemplateTo TemplateBody FilePath - -- ^ Generate to the given path - | UseExistingTemplate FilePath - -- ^ Assume template exists + = -- | Generate at an inferred name + GenerateTemplate TemplateBody TemplateFormat + | -- | Generate to the given path + GenerateTemplateTo TemplateBody FilePath + | -- | Assume template exists + UseExistingTemplate FilePath data TemplateFormat = TemplateFormatYaml @@ -69,21 +69,22 @@ generate Generate {..} = do GenerateTemplate body format -> ( Just body , case format of - TemplateFormatYaml -> unpack (unStackName stackName) <> ".yaml" - TemplateFormatJson -> unpack (unStackName stackName) <> ".json" + TemplateFormatYaml -> unpack (unStackName stackName) <> ".yaml" + TemplateFormatJson -> unpack (unStackName stackName) <> ".json" ) GenerateTemplateTo body path -> (Just body, path) UseExistingTemplate path -> (Nothing, path) - specYaml = StackSpecYaml - { ssyDescription = gDescription - , ssyTemplate = templatePath - , ssyDepends = gDepends - , ssyActions = gActions - , ssyParameters = parametersYaml . mapMaybe parameterYaml <$> gParameters - , ssyCapabilities = gCapabilities - , ssyTags = tagsYaml . map TagYaml <$> gTags - } + specYaml = + StackSpecYaml + { ssyDescription = gDescription + , ssyTemplate = templatePath + , ssyDepends = gDepends + , ssyActions = gActions + , ssyParameters = parametersYaml . mapMaybe parameterYaml <$> gParameters + , ssyCapabilities = gCapabilities + , ssyTags = tagsYaml . map TagYaml <$> gTags + } dir <- view $ directoryOptionL . to unDirectoryOption specPath <- buildSpecPath stackName stackPath diff --git a/src/Stackctl/Spec/List.hs b/src/Stackctl/Spec/List.hs index d96d117..25369fa 100644 --- a/src/Stackctl/Spec/List.hs +++ b/src/Stackctl/Spec/List.hs @@ -1,5 +1,5 @@ module Stackctl.Spec.List - ( ListOptions(..) + ( ListOptions (..) , parseListOptions , runList ) where @@ -12,7 +12,7 @@ import Stackctl.AWS import Stackctl.AWS.Scope import Stackctl.Colors import Stackctl.Config (HasConfig) -import Stackctl.DirectoryOption (HasDirectoryOption(..)) +import Stackctl.DirectoryOption (HasDirectoryOption (..)) import Stackctl.FilterOption (HasFilterOption) import Stackctl.Spec.Discover import Stackctl.StackSpec diff --git a/src/Stackctl/StackDescription.hs b/src/Stackctl/StackDescription.hs index fc95edc..34920e2 100644 --- a/src/Stackctl/StackDescription.hs +++ b/src/Stackctl/StackDescription.hs @@ -1,12 +1,12 @@ module Stackctl.StackDescription - ( StackDescription(..) + ( StackDescription (..) , addStackDescription ) where import Stackctl.Prelude import Control.Lens ((?~)) -import Data.Aeson (FromJSON, Value(..)) +import Data.Aeson (FromJSON, Value (..)) import qualified Data.Aeson as JSON import Data.Aeson.Lens import Data.ByteString.Char8 as BS8 @@ -28,13 +28,15 @@ addStackDescription mStackDescription body = fromMaybe body $ do decodeUtf8 <$> case bc of BodyContentJSON v -> updateJSON d bs <$ guard (not $ hasDescription v) BodyContentYaml v -> updateYaml d bs <$ guard (not $ hasDescription v) - where bs = encodeUtf8 body + where + bs = encodeUtf8 body getBodyContent :: ByteString -> Maybe BodyContent -getBodyContent body = asum - [ BodyContentJSON . Object <$> JSON.decodeStrict body - , hush $ BodyContentYaml . Object <$> Yaml.decodeEither' body - ] +getBodyContent body = + asum + [ BodyContentJSON . Object <$> JSON.decodeStrict body + , hush $ BodyContentYaml . Object <$> Yaml.decodeEither' body + ] -- Inserting a key is easy to do in Yaml without the parsing round-trip that -- would strip formatting and comments. But updating a key is hard. To avoid diff --git a/src/Stackctl/StackSpec.hs b/src/Stackctl/StackSpec.hs index c7e7d55..8cc7c18 100644 --- a/src/Stackctl/StackSpec.hs +++ b/src/Stackctl/StackSpec.hs @@ -27,14 +27,14 @@ import Data.Aeson import qualified Data.ByteString.Lazy as BSL import Data.List.Extra (nubOrdOn) import qualified Data.Yaml as Yaml -import Stackctl.Action import Stackctl.AWS -import Stackctl.Config (HasConfig(..), applyConfig) +import Stackctl.Action +import Stackctl.Config (HasConfig (..), applyConfig) import Stackctl.Sort import Stackctl.StackSpecPath import Stackctl.StackSpecYaml -import qualified System.FilePath as FilePath import System.FilePath (takeExtension) +import qualified System.FilePath as FilePath import UnliftIO.Directory (createDirectoryIfMissing, doesFileExist) data StackSpec = StackSpec @@ -101,11 +101,12 @@ buildStackSpec -> m StackSpec buildStackSpec dir specPath specBody = do config <- view configL - pure StackSpec - { ssSpecRoot = dir - , ssSpecPath = specPath - , ssSpecBody = applyConfig config specBody - } + pure + StackSpec + { ssSpecRoot = dir + , ssSpecPath = specPath + , ssSpecBody = applyConfig config specBody + } data TemplateBody = TemplateText Text @@ -114,7 +115,7 @@ data TemplateBody newtype UnexpectedTemplateJson = UnexpectedTemplateJson { _unexpectedTemplateJsonExtension :: String } - deriving stock Show + deriving stock (Show) instance Exception UnexpectedTemplateJson where displayException (UnexpectedTemplateJson ext) = @@ -192,14 +193,15 @@ createChangeSet -> [Parameter] -> [Tag] -> m (Either Text (Maybe ChangeSet)) -createChangeSet spec parameters tags = awsCloudFormationCreateChangeSet - (stackSpecStackName spec) - (stackSpecStackDescription spec) - (stackSpecTemplate spec) - (nubOrdOn (^. parameter_parameterKey) $ parameters <> stackSpecParameters spec - ) - (stackSpecCapabilities spec) - (nubOrdOn (^. tag_key) $ tags <> stackSpecTags spec) +createChangeSet spec parameters tags = + awsCloudFormationCreateChangeSet + (stackSpecStackName spec) + (stackSpecStackDescription spec) + (stackSpecTemplate spec) + ( nubOrdOn (^. parameter_parameterKey) $ parameters <> stackSpecParameters spec + ) + (stackSpecCapabilities spec) + (nubOrdOn (^. tag_key) $ tags <> stackSpecTags spec) sortStackSpecs :: [StackSpec] -> [StackSpec] sortStackSpecs = sortByDependencies stackSpecStackName stackSpecDepends diff --git a/src/Stackctl/StackSpecPath.hs b/src/Stackctl/StackSpecPath.hs index bc1c970..a5ce673 100644 --- a/src/Stackctl/StackSpecPath.hs +++ b/src/Stackctl/StackSpecPath.hs @@ -3,7 +3,7 @@ module Stackctl.StackSpecPath ( StackSpecPath - -- * Fields + -- * Fields , stackSpecPathAccountId , stackSpecPathAccountName , stackSpecPathRegion @@ -11,7 +11,7 @@ module Stackctl.StackSpecPath , stackSpecPathBasePath , stackSpecPathFilePath - -- * Construction + -- * Construction , stackSpecPath , stackSpecPathFromFilePath ) where @@ -33,12 +33,13 @@ data StackSpecPath = StackSpecPath deriving stock (Eq, Show) stackSpecPath :: AwsScope -> StackName -> FilePath -> StackSpecPath -stackSpecPath sspAwsScope@AwsScope {..} sspStackName sspPath = StackSpecPath - { sspAwsScope - , sspAccountPathPart - , sspStackName - , sspPath - } +stackSpecPath sspAwsScope@AwsScope {..} sspStackName sspPath = + StackSpecPath + { sspAwsScope + , sspAccountPathPart + , sspStackName + , sspPath + } where sspAccountPathPart = unpack $ unAccountId awsAccountId <> "." <> awsAccountName @@ -71,7 +72,8 @@ stackSpecPathFilePath path = stackSpecPathFromFilePath :: AwsScope - -> FilePath -- ^ Must be relative, @stacks/@ + -> FilePath + -- ^ Must be relative, @stacks/@ -> Either String StackSpecPath stackSpecPathFromFilePath awsScope@AwsScope {..} path = case splitDirectories path of @@ -94,17 +96,17 @@ stackSpecPathFromFilePath awsScope@AwsScope {..} path = stackName <- maybe (Left "Must end in .yaml") (Right . StackName) - $ T.stripSuffix ".yaml" - $ T.intercalate "-" - $ map pack rest - - Right $ StackSpecPath - { sspAwsScope = awsScope { awsAccountName = accountName } - , sspAccountPathPart = pathAccount - , sspStackName = stackName - , sspPath = joinPath rest - } - + $ T.stripSuffix ".yaml" + $ T.intercalate "-" + $ map pack rest + + Right + $ StackSpecPath + { sspAwsScope = awsScope {awsAccountName = accountName} + , sspAccountPathPart = pathAccount + , sspStackName = stackName + , sspPath = joinPath rest + } _ -> Left $ "Path is not stacks/././.: " <> path -- | Handle @{account-name}.{account-id}@ or @{account-id}.{account-name}@ @@ -116,4 +118,5 @@ parseAccountPath path = case second (T.drop 1) $ T.breakOn "." $ pack path of Left $ "Path matches neither {account-id}.{account-name}, nor {account-name}.{account-id}: " <> path - where isAccountId x = T.length x == 12 && T.all isDigit x + where + isAccountId x = T.length x == 12 && T.all isDigit x diff --git a/src/Stackctl/StackSpecYaml.hs b/src/Stackctl/StackSpecYaml.hs index ea10960..bc13edd 100644 --- a/src/Stackctl/StackSpecYaml.hs +++ b/src/Stackctl/StackSpecYaml.hs @@ -17,9 +17,8 @@ -- - Key: -- Value: -- @ --- module Stackctl.StackSpecYaml - ( StackSpecYaml(..) + ( StackSpecYaml (..) , ParametersYaml , parametersYaml , unParametersYaml @@ -29,7 +28,7 @@ module Stackctl.StackSpecYaml , TagsYaml , tagsYaml , unTagsYaml - , TagYaml(..) + , TagYaml (..) ) where import Stackctl.Prelude @@ -40,10 +39,10 @@ import qualified Data.Aeson.Key as Key import qualified Data.Aeson.KeyMap as KeyMap import Data.Aeson.Types (typeMismatch) import qualified Data.HashMap.Strict as HashMap -import Data.Monoid (Last(..)) +import Data.Monoid (Last (..)) import qualified Data.Text as T -import Stackctl.Action import Stackctl.AWS +import Stackctl.Action data StackSpecYaml = StackSpecYaml { ssyDescription :: Maybe StackDescription @@ -86,7 +85,7 @@ instance FromJSON ParametersYaml where -- error messages will include "Parameters.{k}". See specs for an example. let parseKey k = ParameterYaml k <$> o .: k ParametersYaml <$> traverse parseKey (KeyMap.keys o) - v@Array{} -> ParametersYaml <$> parseJSON v + v@Array {} -> ParametersYaml <$> parseJSON v v -> typeMismatch err v where err = @@ -164,14 +163,14 @@ instance Semigroup TagsYaml where instance FromJSON TagsYaml where parseJSON = \case Object o -> do - let - parseKey k = do - t <- newTag (Key.toText k) <$> o .: k - pure $ TagYaml t + let parseKey k = do + t <- newTag (Key.toText k) <$> o .: k + pure $ TagYaml t TagsYaml <$> traverse parseKey (KeyMap.keys o) - v@Array{} -> TagsYaml <$> parseJSON v + v@Array {} -> TagsYaml <$> parseJSON v v -> typeMismatch err v - where err = "Object or list of {Key, Value} Objects" + where + err = "Object or list of {Key, Value} Objects" instance ToJSON TagsYaml where toJSON = object . tagsYamlPairs diff --git a/src/Stackctl/Subcommand.hs b/src/Stackctl/Subcommand.hs index ac296e6..83f49fc 100644 --- a/src/Stackctl/Subcommand.hs +++ b/src/Stackctl/Subcommand.hs @@ -1,5 +1,5 @@ module Stackctl.Subcommand - ( Subcommand(..) + ( Subcommand (..) , subcommand , runSubcommand , runSubcommand' @@ -42,12 +42,14 @@ runSubcommand' -> Mod CommandFields (options -> IO a) -> IO a runSubcommand' title parseEnv parseCLI sp = do - (options, act) <- applyEnv - <$> Env.parse (Env.header $ unpack title) parseEnv - <*> execParser (withInfo title $ (,) <$> parseCLI <*> subparser sp) + (options, act) <- + applyEnv + <$> Env.parse (Env.header $ unpack title) parseEnv + <*> execParser (withInfo title $ (,) <$> parseCLI <*> subparser sp) act options - where applyEnv env = first (env <>) + where + applyEnv env = first (env <>) -- | Use this in the 'run' member of a 'Subcommand' that wants 'AppT' -- @@ -60,7 +62,6 @@ runSubcommand' title parseEnv parseCLI sp = do -- runFoo :: (MonadReader env m, HasAws env) => FooOptions -> m () -- runFoo = undefined -- @ --- runAppSubcommand :: ( HasColorOption options , HasVerboseOption options diff --git a/src/Stackctl/TagOption.hs b/src/Stackctl/TagOption.hs index 5988137..0efb80e 100644 --- a/src/Stackctl/TagOption.hs +++ b/src/Stackctl/TagOption.hs @@ -9,12 +9,14 @@ 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" - ] +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 @@ -22,4 +24,5 @@ readTag s = case T.breakOn "=" t of (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 + where + t = pack s diff --git a/src/Stackctl/VerboseOption.hs b/src/Stackctl/VerboseOption.hs index 56443b5..9359c2c 100644 --- a/src/Stackctl/VerboseOption.hs +++ b/src/Stackctl/VerboseOption.hs @@ -1,7 +1,7 @@ module Stackctl.VerboseOption ( Verbosity , verbositySetLogLevels - , HasVerboseOption(..) + , HasVerboseOption (..) , verboseOption ) where @@ -31,8 +31,12 @@ instance HasVerboseOption Verbosity where verboseOptionL = id verboseOption :: Parser Verbosity -verboseOption = fmap Verbosity $ many $ flag' () $ mconcat - [ short 'v' - , long "verbose" - , help "Increase verbosity (can be passed multiple times)" - ] +verboseOption = + fmap Verbosity + $ many + $ flag' () + $ mconcat + [ short 'v' + , long "verbose" + , help "Increase verbosity (can be passed multiple times)" + ] diff --git a/test/Spec.hs b/test/Spec.hs index 545b063..c5e6791 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -1,2 +1,2 @@ -{-# OPTIONS_GHC -fno-warn-missing-export-lists #-} {-# OPTIONS_GHC -F -pgmF hspec-discover #-} +{-# OPTIONS_GHC -fno-warn-missing-export-lists #-} diff --git a/test/Stackctl/AWS/ScopeSpec.hs b/test/Stackctl/AWS/ScopeSpec.hs index 1388177..8dba6f6 100644 --- a/test/Stackctl/AWS/ScopeSpec.hs +++ b/test/Stackctl/AWS/ScopeSpec.hs @@ -12,12 +12,12 @@ import Test.Hspec spec :: Spec spec = do describe "awsScopeSpecStackName" $ do - let - scope = AwsScope - { awsAccountId = AccountId "123" - , awsAccountName = "testing" - , awsRegion = "us-east-1" - } + let scope = + AwsScope + { awsAccountId = AccountId "123" + , awsAccountName = "testing" + , awsRegion = "us-east-1" + } it "parses full paths to stacks in the current scope" $ do awsScopeSpecStackName scope "stacks/123.testing/us-east-1/foo.yaml" diff --git a/test/Stackctl/Config/RequiredVersionSpec.hs b/test/Stackctl/Config/RequiredVersionSpec.hs index 27e2475..7cbfc85 100644 --- a/test/Stackctl/Config/RequiredVersionSpec.hs +++ b/test/Stackctl/Config/RequiredVersionSpec.hs @@ -39,7 +39,6 @@ spec = do it "compares with >=" $ prop (>=) $ Just ">=" it "compares with =~" $ prop (=~) $ Just "=~" - describe "=~" $ do it "treats equal versions as satisfying" $ do makeVersion [1, 2, 3] =~ makeVersion [1, 2, 3] `shouldBe` True @@ -83,4 +82,5 @@ runRequiredVersion -> Either String Bool runRequiredVersion mOperator required current = (`isRequiredVersionSatisfied` current) <$> requiredVersionFromText rvText - where rvText = maybe "" (<> " ") mOperator <> pack (showVersion required) + where + rvText = maybe "" (<> " ") mOperator <> pack (showVersion required) diff --git a/test/Stackctl/ConfigSpec.hs b/test/Stackctl/ConfigSpec.hs index 3950f2e..971c25b 100644 --- a/test/Stackctl/ConfigSpec.hs +++ b/test/Stackctl/ConfigSpec.hs @@ -19,15 +19,15 @@ spec :: Spec spec = do describe "loadConfigFromBytes" $ do it "loads a valid config" $ do - let - result = loadConfigFromLines - [ "required_version: " <> BS8.pack (showVersion Paths.version) - , "defaults:" - , " parameters:" - , " Some: Parameter" - , " tags:" - , " Some: Tag" - ] + let result = + loadConfigFromLines + [ "required_version: " <> BS8.pack (showVersion Paths.version) + , "defaults:" + , " parameters:" + , " Some: Parameter" + , " tags:" + , " Some: Tag" + ] case result of Left err -> do @@ -42,15 +42,16 @@ spec = do describe "applyConfig" $ do it "defaults missing Tags" $ do let - specYaml = StackSpecYaml - { ssyDescription = Nothing - , ssyTemplate = "" - , ssyDepends = Nothing - , ssyActions = Nothing - , ssyParameters = Nothing - , ssyCapabilities = Nothing - , ssyTags = Just $ toTagsYaml [("Hi", "There"), ("Keep", "Me")] - } + specYaml = + StackSpecYaml + { ssyDescription = Nothing + , ssyTemplate = "" + , ssyDepends = Nothing + , ssyActions = Nothing + , ssyParameters = Nothing + , ssyCapabilities = Nothing + , ssyTags = Just $ toTagsYaml [("Hi", "There"), ("Keep", "Me")] + } Right config = loadConfigFromBytes @@ -61,8 +62,9 @@ spec = do Just tags = ssyTags (applyConfig config specYaml) - tags `shouldBe` toTagsYaml - [("From", "Defaults"), ("Hi", "There"), ("Keep", "Me")] + tags + `shouldBe` toTagsYaml + [("From", "Defaults"), ("Hi", "There"), ("Keep", "Me")] loadConfigFromLines :: MonadError ConfigError m => [ByteString] -> m Config loadConfigFromLines = loadConfigFromBytes . mconcat . map (<> "\n") diff --git a/test/Stackctl/FilterOptionSpec.hs b/test/Stackctl/FilterOptionSpec.hs index e905c5d..ed88652 100644 --- a/test/Stackctl/FilterOptionSpec.hs +++ b/test/Stackctl/FilterOptionSpec.hs @@ -91,8 +91,9 @@ spec = do describe "filterOptionFromPaths" $ do it "finds full paths (e.g. as output by generate)" $ do let - option = filterOptionFromPaths - $ pure "stacks/1234567890.test-account/us-east-1/stack.yaml" + option = + filterOptionFromPaths + $ pure "stacks/1234567890.test-account/us-east-1/stack.yaml" specs = [ toSpec "some-name" "stack.yaml" Nothing , toSpec "other-path" "other-stack.yaml" $ Just "x" @@ -102,26 +103,29 @@ spec = do `shouldMatchList` ["some-name"] toSpec :: Text -> FilePath -> Maybe FilePath -> StackSpec -toSpec name path mTemplate = flip runReader emptyConfig - $ buildStackSpec ".platform/specs" specPath specBody +toSpec name path mTemplate = + flip runReader emptyConfig + $ buildStackSpec ".platform/specs" specPath specBody where stackName = StackName name specPath = stackSpecPath scope stackName path - specBody = StackSpecYaml - { ssyDescription = Nothing - , ssyDepends = Nothing - , ssyActions = Nothing - , ssyTemplate = fromMaybe path mTemplate - , ssyParameters = Nothing - , ssyCapabilities = Nothing - , ssyTags = Nothing - } - - scope = AwsScope - { awsAccountId = AccountId "1234567890" - , awsAccountName = "test-account" - , awsRegion = Region' "us-east-1" - } + specBody = + StackSpecYaml + { ssyDescription = Nothing + , ssyDepends = Nothing + , ssyActions = Nothing + , ssyTemplate = fromMaybe path mTemplate + , ssyParameters = Nothing + , ssyCapabilities = Nothing + , ssyTags = Nothing + } + + scope = + AwsScope + { awsAccountId = AccountId "1234567890" + , awsAccountName = "test-account" + , awsRegion = Region' "us-east-1" + } specName :: StackSpec -> Text specName = unStackName . stackSpecStackName diff --git a/test/Stackctl/StackDescriptionSpec.hs b/test/Stackctl/StackDescriptionSpec.hs index fa6d383..6f99346 100644 --- a/test/Stackctl/StackDescriptionSpec.hs +++ b/test/Stackctl/StackDescriptionSpec.hs @@ -26,8 +26,8 @@ spec = do it "does not clobber or duplicate an existing Description" $ do addStackDescription - aDescription - "Resources: []\nDescription: Existing description\n" + aDescription + "Resources: []\nDescription: Existing description\n" `shouldBe` "Resources: []\nDescription: Existing description\n" context "JSON" $ do @@ -37,6 +37,6 @@ spec = do it "does not clobber or duplicate an existing Description" $ do addStackDescription - aDescription - "{\"Resources\":[],\"Description\":\"Existing description\"}" + aDescription + "{\"Resources\":[],\"Description\":\"Existing description\"}" `shouldBe` "{\"Resources\":[],\"Description\":\"Existing description\"}" diff --git a/test/Stackctl/StackSpecSpec.hs b/test/Stackctl/StackSpecSpec.hs index a16b3cb..30105d5 100644 --- a/test/Stackctl/StackSpecSpec.hs +++ b/test/Stackctl/StackSpecSpec.hs @@ -16,38 +16,40 @@ spec :: Spec spec = do describe "sortStackSpecs" $ do it "orders dependencies before dependents" $ do - let - specs = - [ toSpec "app" ["roles", "iam", "networking"] - , toSpec "roles" ["iam"] - , toSpec "iam" [] - , toSpec "networking" [] - ] + let specs = + [ toSpec "app" ["roles", "iam", "networking"] + , toSpec "roles" ["iam"] + , toSpec "iam" [] + , toSpec "networking" [] + ] map specName (sortStackSpecs specs) `shouldBe` ["iam", "roles", "networking", "app"] toSpec :: Text -> [Text] -> StackSpec -toSpec name depends = flip runReader emptyConfig - $ buildStackSpec "." specPath specBody +toSpec name depends = + flip runReader emptyConfig + $ buildStackSpec "." specPath specBody where stackName = StackName name specPath = stackSpecPath scope stackName "a/b.yaml" - specBody = StackSpecYaml - { ssyDescription = Nothing - , ssyDepends = Just $ map StackName depends - , ssyActions = Nothing - , ssyTemplate = "" - , ssyParameters = Nothing - , ssyCapabilities = Nothing - , ssyTags = Nothing - } + specBody = + StackSpecYaml + { ssyDescription = Nothing + , ssyDepends = Just $ map StackName depends + , ssyActions = Nothing + , ssyTemplate = "" + , ssyParameters = Nothing + , ssyCapabilities = Nothing + , ssyTags = Nothing + } - scope = AwsScope - { awsAccountId = AccountId "" - , awsAccountName = "" - , awsRegion = Region' "" - } + scope = + AwsScope + { awsAccountId = AccountId "" + , awsAccountName = "" + , awsRegion = Region' "" + } specName :: StackSpec -> Text specName = unStackName . stackSpecStackName diff --git a/test/Stackctl/StackSpecYamlSpec.hs b/test/Stackctl/StackSpecYamlSpec.hs index 53a4e7b..91942dc 100644 --- a/test/Stackctl/StackSpecYamlSpec.hs +++ b/test/Stackctl/StackSpecYamlSpec.hs @@ -8,8 +8,8 @@ import Stackctl.Prelude import Data.Aeson import qualified Data.Yaml as Yaml -import Stackctl.Action import Stackctl.AWS +import Stackctl.Action import Stackctl.StackSpecYaml import Test.Hspec @@ -17,123 +17,135 @@ spec :: Spec spec = do describe "From/ToJSON" $ do it "round trips" $ do - let - yaml = StackSpecYaml - { ssyDescription = Just $ StackDescription "Testing Stack" - , ssyTemplate = "path/to/template.yaml" - , ssyDepends = Just [StackName "a-stack", StackName "another-stack"] - , ssyActions = Just - [newAction PostDeploy $ InvokeLambdaByName "a-lambda"] - , ssyParameters = Just $ parametersYaml $ mapMaybe - parameterYaml - [makeParameter "PKey" $ Just "PValue"] - , ssyCapabilities = Just [Capability_CAPABILITY_IAM] - , ssyTags = Just $ tagsYaml [TagYaml $ newTag "TKey" "TValue"] - } + let yaml = + StackSpecYaml + { ssyDescription = Just $ StackDescription "Testing Stack" + , ssyTemplate = "path/to/template.yaml" + , ssyDepends = Just [StackName "a-stack", StackName "another-stack"] + , ssyActions = + Just + [newAction PostDeploy $ InvokeLambdaByName "a-lambda"] + , ssyParameters = + Just + $ parametersYaml + $ mapMaybe + parameterYaml + [makeParameter "PKey" $ Just "PValue"] + , ssyCapabilities = Just [Capability_CAPABILITY_IAM] + , ssyTags = Just $ tagsYaml [TagYaml $ newTag "TKey" "TValue"] + } eitherDecode (encode yaml) `shouldBe` Right yaml describe "decoding Yaml" $ do it "reads String parameters" $ do - StackSpecYaml {..} <- Yaml.decodeThrow $ mconcat - [ "Template: foo.yaml\n" - , "Parameters:\n" - , " - ParameterKey: Foo\n" - , " ParameterValue: Bar\n" - ] - - let - Just [param] = map unParameterYaml . unParametersYaml <$> ssyParameters + StackSpecYaml {..} <- + Yaml.decodeThrow + $ mconcat + [ "Template: foo.yaml\n" + , "Parameters:\n" + , " - ParameterKey: Foo\n" + , " ParameterValue: Bar\n" + ] + + let Just [param] = map unParameterYaml . unParametersYaml <$> ssyParameters param ^. parameter_parameterKey `shouldBe` Just "Foo" param ^. parameter_parameterValue `shouldBe` Just "Bar" it "reads Number parameters without decimals" $ do - StackSpecYaml {..} <- Yaml.decodeThrow $ mconcat - [ "Template: foo.yaml\n" - , "Parameters:\n" - , " - ParameterKey: Port\n" - , " ParameterValue: 80\n" - ] - - let - Just [param] = map unParameterYaml . unParametersYaml <$> ssyParameters + StackSpecYaml {..} <- + Yaml.decodeThrow + $ mconcat + [ "Template: foo.yaml\n" + , "Parameters:\n" + , " - ParameterKey: Port\n" + , " ParameterValue: 80\n" + ] + + let Just [param] = map unParameterYaml . unParametersYaml <$> ssyParameters param ^. parameter_parameterKey `shouldBe` Just "Port" param ^. parameter_parameterValue `shouldBe` Just "80" it "reads Number parameters with decimals" $ do - StackSpecYaml {..} <- Yaml.decodeThrow $ mconcat - [ "Template: foo.yaml\n" - , "Parameters:\n" - , " - ParameterKey: Pie\n" - , " ParameterValue: 3.14\n" - ] - - let - Just [param] = map unParameterYaml . unParametersYaml <$> ssyParameters + StackSpecYaml {..} <- + Yaml.decodeThrow + $ mconcat + [ "Template: foo.yaml\n" + , "Parameters:\n" + , " - ParameterKey: Pie\n" + , " ParameterValue: 3.14\n" + ] + + let Just [param] = map unParameterYaml . unParametersYaml <$> ssyParameters param ^. parameter_parameterKey `shouldBe` Just "Pie" param ^. parameter_parameterValue `shouldBe` Just "3.14" it "has informative errors" $ do - let - Left ex = Yaml.decodeEither' @StackSpecYaml $ mconcat - [ "Template: foo.yaml\n" - , "Parameters:\n" - , " - ParameterKey: Norway\n" - , " ParameterValue: no\n" - ] + let Left ex = + Yaml.decodeEither' @StackSpecYaml + $ mconcat + [ "Template: foo.yaml\n" + , "Parameters:\n" + , " - ParameterKey: Norway\n" + , " ParameterValue: no\n" + ] show ex `shouldBe` "AesonException \"Error in $.Parameters[0].ParameterValue: Expected String or Number, got: Bool False\"" it "has informative errors in Object form" $ do - let - Left ex = Yaml.decodeEither' @StackSpecYaml $ mconcat - ["Template: foo.yaml\n", "Parameters:\n", " Norway: no\n"] + let Left ex = + Yaml.decodeEither' @StackSpecYaml + $ mconcat + ["Template: foo.yaml\n", "Parameters:\n", " Norway: no\n"] show ex `shouldBe` "AesonException \"Error in $.Parameters.Norway: Expected String or Number, got: Bool False\"" it "handles null Value" $ do - StackSpecYaml {..} <- Yaml.decodeThrow $ mconcat - [ "Template: foo.yaml\n" - , "Parameters:\n" - , " - ParameterKey: Foo\n" - , " ParameterValue: null\n" - ] - - let - Just [param] = map unParameterYaml . unParametersYaml <$> ssyParameters + StackSpecYaml {..} <- + Yaml.decodeThrow + $ mconcat + [ "Template: foo.yaml\n" + , "Parameters:\n" + , " - ParameterKey: Foo\n" + , " ParameterValue: null\n" + ] + + let Just [param] = map unParameterYaml . unParametersYaml <$> ssyParameters param ^. parameter_parameterKey `shouldBe` Just "Foo" param ^. parameter_parameterValue `shouldBe` Nothing it "handles missing Value" $ do - StackSpecYaml {..} <- Yaml.decodeThrow $ mconcat - ["Template: foo.yaml\n", "Parameters:\n", " - ParameterKey: Foo\n"] + StackSpecYaml {..} <- + Yaml.decodeThrow + $ mconcat + ["Template: foo.yaml\n", "Parameters:\n", " - ParameterKey: Foo\n"] - let - Just [param] = map unParameterYaml . unParametersYaml <$> ssyParameters + let Just [param] = map unParameterYaml . unParametersYaml <$> ssyParameters param ^. parameter_parameterKey `shouldBe` Just "Foo" param ^. parameter_parameterValue `shouldBe` Nothing it "also accepts CloudGenesis formatted values" $ do - StackSpecYaml {..} <- Yaml.decodeThrow $ mconcat - [ "Template: foo.yaml\n" - , "Parameters:\n" - , " - Name: Foo\n" - , " Value: Bar\n" - ] - - let - Just [param] = map unParameterYaml . unParametersYaml <$> ssyParameters + StackSpecYaml {..} <- + Yaml.decodeThrow + $ mconcat + [ "Template: foo.yaml\n" + , "Parameters:\n" + , " - Name: Foo\n" + , " Value: Bar\n" + ] + + let Just [param] = map unParameterYaml . unParametersYaml <$> ssyParameters param ^. parameter_parameterKey `shouldBe` Just "Foo" param ^. parameter_parameterValue `shouldBe` Just "Bar" it "also accepts objects" $ do - StackSpecYaml {..} <- Yaml.decodeThrow - $ mconcat ["Template: foo.yaml\n", "Parameters:\n", " Foo: Bar\n"] + StackSpecYaml {..} <- + Yaml.decodeThrow + $ mconcat ["Template: foo.yaml\n", "Parameters:\n", " Foo: Bar\n"] - let - Just [param] = map unParameterYaml . unParametersYaml <$> ssyParameters + let Just [param] = map unParameterYaml . unParametersYaml <$> ssyParameters param ^. parameter_parameterKey `shouldBe` Just "Foo" param ^. parameter_parameterValue `shouldBe` Just "Bar" @@ -141,18 +153,20 @@ spec = do it "has overriding Semigroup semantics" $ do let a = parametersYaml [] - b = parametersYaml - $ catMaybes [parameterYaml $ makeParameter "Key" (Just "B")] - c = parametersYaml - $ catMaybes [parameterYaml $ makeParameter "Key" (Just "C")] - d = parametersYaml - $ catMaybes [parameterYaml $ makeParameter "Key" Nothing] + b = + parametersYaml + $ catMaybes [parameterYaml $ makeParameter "Key" (Just "B")] + c = + parametersYaml + $ catMaybes [parameterYaml $ makeParameter "Key" (Just "C")] + d = + parametersYaml + $ catMaybes [parameterYaml $ makeParameter "Key" Nothing] a <> b `shouldBe` b -- keeps keys in B b <> c `shouldBe` c -- C overrides B (Last) c <> d `shouldBe` c -- C overrides D (Just) d <> c `shouldBe` c -- C overrides D (Just) - describe "TagsYaml" $ do it "has overriding Semigroup semantics" $ do let From 45e41a6c8feebf4e51dd4ac899d85624917f5370 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Fri, 30 Jun 2023 09:05:36 -0400 Subject: [PATCH 068/187] Handle AWS ServiceError to more readable exit Any un-handled AWS errors would crash with a shown exception that is difficult to read. This handler just reformats it to a more readable error-log before still exiting failure. We implemented this in a downstream user of the library, but it makes sense to be in use all the time and provided by us. --- src/Stackctl/AWS/Core.hs | 21 +++++++++++++++++++++ src/Stackctl/Subcommand.hs | 6 +++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/Stackctl/AWS/Core.hs b/src/Stackctl/AWS/Core.hs index 21f2f2b..d413293 100644 --- a/src/Stackctl/AWS/Core.hs +++ b/src/Stackctl/AWS/Core.hs @@ -15,6 +15,9 @@ module Stackctl.AWS.Core -- * 'Amazonka' extensions , AccountId (..) + -- * Error-handling + , handlingServiceError + -- * 'Amazonka'/'ResourceT' re-exports , Region (..) , FromText (..) @@ -143,3 +146,21 @@ newtype AccountId = AccountId { unAccountId :: Text } deriving newtype (Eq, Ord, Show, ToJSON) + +-- | Handle 'ServiceError', log it and 'exitFailure' +-- +-- This is useful at the top-level of the app, where we'd be crashing anyway. It +-- makes things more readable and easier to debug. +handlingServiceError :: (MonadUnliftIO m, MonadLogger m) => m a -> m a +handlingServiceError = + handleJust @_ @SomeException (^? _ServiceError) $ \e -> do + logError + $ "Exiting due to AWS Service error" + :# [ "code" .= fromErrorCode (e ^. serviceCode) + , "message" .= fmap fromErrorMessage (e ^. serviceMessage) + , "requestId" .= fmap fromRequestId (e ^. serviceRequestId) + ] + exitFailure + +fromErrorCode :: ErrorCode -> Text +fromErrorCode (ErrorCode x) = x diff --git a/src/Stackctl/Subcommand.hs b/src/Stackctl/Subcommand.hs index 83f49fc..eb84864 100644 --- a/src/Stackctl/Subcommand.hs +++ b/src/Stackctl/Subcommand.hs @@ -10,6 +10,7 @@ import Stackctl.Prelude import qualified Env import Options.Applicative +import Stackctl.AWS (handlingServiceError) import Stackctl.AutoSSO import Stackctl.CLI import Stackctl.ColorOption @@ -71,7 +72,10 @@ runAppSubcommand -> subOptions -> options -> IO a -runAppSubcommand f subOptions options = runAppT options $ f subOptions +runAppSubcommand f subOptions options = + runAppT options + $ handlingServiceError + $ f subOptions withInfo :: Text -> Parser a -> ParserInfo a withInfo d p = info (p <**> helper) $ progDesc (unpack d) <> fullDesc From 6afb63a26fc1eb249055a3df7f8cd7ee2ac166af Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Fri, 30 Jun 2023 13:49:27 -0400 Subject: [PATCH 069/187] Handle removals before deploys It can be useful to deploy a single change set that deletes a stack using some name, then deploys a new one that uses that name. This doesn't work when we handle deletes after deploys. Reversing it handles that use-case better, without any downsides. --- src/Stackctl/Spec/Deploy.hs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Stackctl/Spec/Deploy.hs b/src/Stackctl/Spec/Deploy.hs index f3518d6..a9d6c2e 100644 --- a/src/Stackctl/Spec/Deploy.hs +++ b/src/Stackctl/Spec/Deploy.hs @@ -84,6 +84,10 @@ runDeploy => DeployOptions -> m () runDeploy DeployOptions {..} = do + when sdoRemovals $ do + removed <- inferRemovedStacks + traverse_ (deleteRemovedStack sdoDeployConfirmation) removed + specs <- discoverSpecs for_ specs $ \spec -> do @@ -111,10 +115,6 @@ runDeploy DeployOptions {..} = do runActions stackName PostDeploy $ stackSpecActions spec when sdoClean $ awsCloudFormationDeleteAllChangeSets stackName - when sdoRemovals $ do - removed <- inferRemovedStacks - traverse_ (deleteRemovedStack sdoDeployConfirmation) removed - deleteRemovedStack :: ( MonadMask m , MonadResource m From 9afbdfc834309a1726a823fb95ef9902ec626a08 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 14 Jun 2023 09:16:27 -0400 Subject: [PATCH 070/187] Use Blammo.Logging.Colors --- package.yaml | 2 +- src/Stackctl/Colors.hs | 28 +--------------------------- stack.yaml | 2 +- stack.yaml.lock | 8 ++++---- stackctl.cabal | 2 +- 5 files changed, 8 insertions(+), 34 deletions(-) diff --git a/package.yaml b/package.yaml index b6c07e0..c5bd951 100644 --- a/package.yaml +++ b/package.yaml @@ -58,7 +58,7 @@ default-extensions: library: source-dirs: src dependencies: - - Blammo >= 1.1.1.1 # pushLoggerLn, getLoggerShouldColor + - Blammo >= 1.1.2.1 # getColorsLogger, etc - Glob - QuickCheck - aeson diff --git a/src/Stackctl/Colors.hs b/src/Stackctl/Colors.hs index b7283d2..d51d781 100644 --- a/src/Stackctl/Colors.hs +++ b/src/Stackctl/Colors.hs @@ -1,31 +1,5 @@ --- | Facilities for colorizing output module Stackctl.Colors - ( Colors (..) - , getColorsStdout - , getColorsLogger - , noColors + ( module Blammo.Logging.Colors ) where -import Stackctl.Prelude - import Blammo.Logging.Colors -import Blammo.Logging.LogSettings (shouldColorHandle) -import Blammo.Logging.Logger - --- | Return 'Colors' based on options and 'stdout' -getColorsStdout :: (MonadIO m, MonadReader env m, HasLogger env) => m Colors -getColorsStdout = getColorsHandle stdout - --- | Return 'Colors' based on options given 'Handle' -getColorsHandle - :: (MonadIO m, MonadReader env m, HasLogger env) => Handle -> m Colors -getColorsHandle h = do - ls <- view $ loggerL . to getLoggerLogSettings - getColors <$> shouldColorHandle ls h - --- | Return 'Colors' consistent with the ambient 'Logger' -getColorsLogger :: (MonadReader env m, HasLogger env) => m Colors -getColorsLogger = view $ loggerL . to (getColors . getLoggerShouldColor) - -noColors :: Colors -noColors = getColors False diff --git a/stack.yaml b/stack.yaml index f5cf943..002dc35 100644 --- a/stack.yaml +++ b/stack.yaml @@ -1,7 +1,7 @@ resolver: lts-20.4 extra-deps: - - Blammo-1.1.1.1 + - Blammo-1.1.2.1 - cfn-flip-0.1.0.3 - unliftio-0.2.25.0 diff --git a/stack.yaml.lock b/stack.yaml.lock index 5b3ab93..c5f0be2 100644 --- a/stack.yaml.lock +++ b/stack.yaml.lock @@ -5,12 +5,12 @@ packages: - completed: - hackage: Blammo-1.1.1.1@sha256:2a40212b058e49f0449cd81a786a216a97ec1e4139870e571560312b50532430,4045 + hackage: Blammo-1.1.2.1@sha256:b74d553fb3557bb10381b806bd34b8bad0b800883f02dfd1cc847f58db40958c,4084 pantry-tree: - sha256: 2dc64fe1800fbb344ae8345762dc814e6014147671fe0749581b2fe1c6ed9a92 - size: 1490 + sha256: bd28931f07beaaae8565a87d8c3b55d3e9ff5c332ae93dc32c1090a4c814e620 + size: 1567 original: - hackage: Blammo-1.1.1.1 + hackage: Blammo-1.1.2.1 - completed: hackage: cfn-flip-0.1.0.3@sha256:8737882d818d74b29d3b1791a4df4dc89995870312374989c47c29352ea503ec,5615 pantry-tree: diff --git a/stackctl.cabal b/stackctl.cabal index 7e4b7b8..b3e6bed 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -97,7 +97,7 @@ library TypeFamilies ghc-options: -fignore-optim-changes -fwrite-ide-info -Weverything -Wno-all-missed-specialisations -Wno-missing-import-lists -Wno-missing-kind-signatures -Wno-missing-local-signatures -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module -Wno-unsafe -optP-Wno-nonportable-include-path build-depends: - Blammo >=1.1.1.1 + Blammo >=1.1.2.1 , Glob , QuickCheck , aeson From e35504e3833667500b69495442e8b9a19d24a814 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Mon, 28 Aug 2023 13:11:38 -0400 Subject: [PATCH 071/187] Amazonka 2.0 --- package.yaml | 14 +-- src/Stackctl/AWS/CloudFormation.hs | 9 -- src/Stackctl/AWS/Core.hs | 90 ++++++++++++------- src/Stackctl/AutoSSO.hs | 7 +- stack.yaml | 24 +++--- stack.yaml.lock | 133 ++++++++++------------------- stackctl.cabal | 14 +-- 7 files changed, 133 insertions(+), 158 deletions(-) diff --git a/package.yaml b/package.yaml index c5bd951..546067e 100644 --- a/package.yaml +++ b/package.yaml @@ -64,13 +64,13 @@ library: - aeson - aeson-casing - aeson-pretty - - amazonka - - amazonka-cloudformation - - amazonka-core - - amazonka-ec2 - - amazonka-lambda - - amazonka-sso - - amazonka-sts + - amazonka >= 2.0 + - amazonka-cloudformation >= 2.0 + - amazonka-core >= 2.0 + - amazonka-ec2 >= 2.0 + - amazonka-lambda >= 2.0 + - amazonka-sso >= 2.0 + - amazonka-sts >= 2.0 - bytestring - cfn-flip >= 0.1.0.3 # bugfix for Condition - conduit diff --git a/src/Stackctl/AWS/CloudFormation.hs b/src/Stackctl/AWS/CloudFormation.hs index 8a24f5e..283f189 100644 --- a/src/Stackctl/AWS/CloudFormation.hs +++ b/src/Stackctl/AWS/CloudFormation.hs @@ -81,8 +81,6 @@ import Amazonka.Core ( AsError , ServiceError , hasStatus - , serviceCode - , serviceMessage , _MatchServiceError , _ServiceError ) @@ -503,10 +501,3 @@ runningStatuses = _ValidationError :: AsError a => Getting (First ServiceError) a ServiceError _ValidationError = _MatchServiceError defaultService "ValidationError" . hasStatus 400 - -formatServiceError :: ServiceError -> Text -formatServiceError e = - mconcat - [ toText $ e ^. serviceCode - , maybe "" ((": " <>) . toText) $ e ^. serviceMessage - ] diff --git a/src/Stackctl/AWS/Core.hs b/src/Stackctl/AWS/Core.hs index d413293..28e1a01 100644 --- a/src/Stackctl/AWS/Core.hs +++ b/src/Stackctl/AWS/Core.hs @@ -17,6 +17,7 @@ module Stackctl.AWS.Core -- * Error-handling , handlingServiceError + , formatServiceError -- * 'Amazonka'/'ResourceT' re-exports , Region (..) @@ -30,6 +31,8 @@ import Stackctl.Prelude hiding (timeout) import Amazonka hiding (LogLevel (..)) import qualified Amazonka as AWS import Amazonka.Auth.Keys (fromSession) +import Amazonka.Data.Text (FromText (..), ToText (..)) +import Amazonka.Env (env_logger, env_region) import Amazonka.STS.AssumeRole import Conduit (ConduitM) import Control.Monad.Logger (defaultLoc, toLogStr) @@ -51,20 +54,19 @@ awsEnvDiscover = do configureLogging :: MonadLoggerIO m => Env -> m Env configureLogging env = do loggerIO <- askLoggerIO - pure - $ env - { AWS.envLogger = \level msg -> do - loggerIO - defaultLoc -- TODO: there may be a way to get a CallStack/Loc - "Amazonka" - ( case level of - AWS.Info -> LevelInfo - AWS.Error -> LevelError - AWS.Debug -> LevelDebug - AWS.Trace -> LevelOther "trace" - ) - (toLogStr msg) - } + + let logger level = do + loggerIO + defaultLoc -- TODO: there may be a way to get a CallStack/Loc + "Amazonka" + ( case level of + AWS.Info -> LevelInfo + AWS.Error -> LevelError + AWS.Debug -> LevelDebug + AWS.Trace -> LevelOther "trace" + ) + . toLogStr + pure $ env & env_logger .~ logger class HasAwsEnv env where awsEnvL :: Lens' env AwsEnv @@ -73,7 +75,13 @@ instance HasAwsEnv AwsEnv where awsEnvL = id awsSimple - :: (MonadResource m, MonadReader env m, HasAwsEnv env, AWSRequest a) + :: ( MonadResource m + , MonadReader env m + , HasAwsEnv env + , AWSRequest a + , Typeable a + , Typeable (AWSResponse a) + ) => Text -> a -> (AWSResponse a -> Maybe b) @@ -85,7 +93,13 @@ awsSimple name req post = do err = unpack name <> " successful, but processing the response failed" awsSend - :: (MonadResource m, MonadReader env m, HasAwsEnv env, AWSRequest a) + :: ( MonadResource m + , MonadReader env m + , HasAwsEnv env + , AWSRequest a + , Typeable a + , Typeable (AWSResponse a) + ) => a -> m (AWSResponse a) awsSend req = do @@ -93,7 +107,13 @@ awsSend req = do send env req awsPaginate - :: (MonadResource m, MonadReader env m, HasAwsEnv env, AWSPager a) + :: ( MonadResource m + , MonadReader env m + , HasAwsEnv env + , AWSPager a + , Typeable a + , Typeable (AWSResponse a) + ) => a -> ConduitM () (AWSResponse a) m () awsPaginate req = do @@ -104,7 +124,12 @@ hoistEither :: MonadIO m => Either Error a -> m a hoistEither = either (liftIO . throwIO) pure awsAwait - :: (MonadResource m, MonadReader env m, HasAwsEnv env, AWSRequest a) + :: ( MonadResource m + , MonadReader env m + , HasAwsEnv env + , AWSRequest a + , Typeable a + ) => Wait a -> a -> m Accept @@ -125,22 +150,22 @@ awsAssumeRole role sessionName f = do let req = newAssumeRole role sessionName assumeEnv <- awsSimple "sts:AssumeRole" req $ \resp -> do - creds <- resp ^. assumeRoleResponse_credentials - token <- creds ^. authSessionToken + let creds = resp ^. assumeRoleResponse_credentials + token <- creds ^. authEnv_sessionToken let - accessKeyId = creds ^. authAccessKeyId - secretAccessKey = creds ^. authSecretAccessKey + accessKeyId = creds ^. authEnv_accessKeyId + secretAccessKey = creds ^. authEnv_secretAccessKey . _Sensitive - pure $ fromSession accessKeyId secretAccessKey token + pure $ fromSession accessKeyId secretAccessKey $ token ^. _Sensitive local (awsEnvL . unL %~ assumeEnv) f awsWithin :: (MonadReader env m, HasAwsEnv env) => Region -> m a -> m a -awsWithin r = local $ over (awsEnvL . unL) (within r) +awsWithin r = local $ awsEnvL . unL . env_region .~ r awsTimeout :: (MonadReader env m, HasAwsEnv env) => Seconds -> m a -> m a -awsTimeout t = local $ over (awsEnvL . unL) (timeout t) +awsTimeout t = local $ over (awsEnvL . unL) (globalTimeout t) newtype AccountId = AccountId { unAccountId :: Text @@ -156,11 +181,16 @@ handlingServiceError = handleJust @_ @SomeException (^? _ServiceError) $ \e -> do logError $ "Exiting due to AWS Service error" - :# [ "code" .= fromErrorCode (e ^. serviceCode) - , "message" .= fmap fromErrorMessage (e ^. serviceMessage) - , "requestId" .= fmap fromRequestId (e ^. serviceRequestId) + :# [ "code" .= toText (e ^. serviceError_code) + , "message" .= fmap toText (e ^. serviceError_message) + , "requestId" .= fmap toText (e ^. serviceError_requestId) ] exitFailure -fromErrorCode :: ErrorCode -> Text -fromErrorCode (ErrorCode x) = x +formatServiceError :: ServiceError -> Text +formatServiceError e = + mconcat + [ toText $ e ^. serviceError_code + , maybe "" ((": " <>) . toText) $ e ^. serviceError_message + , maybe "" (("\nRequest Id: " <>) . toText) $ e ^. serviceError_requestId + ] diff --git a/src/Stackctl/AutoSSO.hs b/src/Stackctl/AutoSSO.hs index 3a791f8..cf20e3a 100644 --- a/src/Stackctl/AutoSSO.hs +++ b/src/Stackctl/AutoSSO.hs @@ -10,12 +10,13 @@ module Stackctl.AutoSSO import Stackctl.Prelude import Amazonka.SSO (_UnauthorizedException) -import Amazonka.Types (Error, ErrorMessage (..), serviceMessage) import Data.Semigroup (Last (..)) import qualified Env import Options.Applicative +import Stackctl.AWS.Core (formatServiceError) import Stackctl.Prompt import System.Process.Typed +import UnliftIO.Exception.Lens (catching) data AutoSSOOption = AutoSSOAlways @@ -61,7 +62,7 @@ handleAutoSSO -> m a -> m a handleAutoSSO options f = do - catchJust (preview (_UnauthorizedException @Error)) f $ \ex -> do + catching _UnauthorizedException f $ \ex -> do case options ^. autoSSOOptionL of AutoSSOAlways -> do logWarn $ ssoErrorMessage ex @@ -78,6 +79,6 @@ handleAutoSSO options f = do where ssoErrorMessage ex = "AWS SSO authorization error" - :# [ "message" .= fmap fromErrorMessage (ex ^. serviceMessage) + :# [ "message" .= formatServiceError ex , "hint" .= ("Run `aws sso login' and try again" :: Text) ] diff --git a/stack.yaml b/stack.yaml index 002dc35..3a4757c 100644 --- a/stack.yaml +++ b/stack.yaml @@ -5,15 +5,15 @@ extra-deps: - cfn-flip-0.1.0.3 - unliftio-0.2.25.0 - - github: brendanhay/amazonka - commit: f73a957d05f64863e867cf39d0db260718f0fadd # main, as of SSO support - subdirs: - - lib/amazonka - - lib/amazonka-core - - lib/services/amazonka-certificatemanager - - lib/services/amazonka-cloudformation - - lib/services/amazonka-ec2 - - lib/services/amazonka-ecr - - lib/services/amazonka-lambda - - lib/services/amazonka-sso - - lib/services/amazonka-sts + - amazonka-2.0 + - amazonka-core-2.0 + - amazonka-certificatemanager-2.0 + - amazonka-cloudformation-2.0 + - amazonka-ec2-2.0 + - amazonka-ecr-2.0 + - amazonka-lambda-2.0 + - amazonka-sso-2.0 + - amazonka-sts-2.0 + + # For amazonka-core-2.0 + - crypton-0.33 diff --git a/stack.yaml.lock b/stack.yaml.lock index c5f0be2..7659034 100644 --- a/stack.yaml.lock +++ b/stack.yaml.lock @@ -26,122 +26,75 @@ packages: original: hackage: unliftio-0.2.25.0 - completed: - name: amazonka + hackage: amazonka-2.0@sha256:3481da2fda6b210d15d41c1db7a588adf68123cfb7ea3882797a6230003259db,3505 pantry-tree: - sha256: 0257a27c3332e400abc0f4a38f7a875c4a2a04b03ac342d7481e19d9d5665040 - size: 1257 - sha256: 14aeaa9f748f7ac03683e8a8126760ed16aa82152404a96c0333b582444cd381 - size: 27775608 - subdir: lib/amazonka - url: https://github.com/brendanhay/amazonka/archive/f73a957d05f64863e867cf39d0db260718f0fadd.tar.gz - version: '2.0' + sha256: 01c7121bd5e4a3918a71ea6502412292c97facf20c9620f07af96e423d6437e2 + size: 1528 original: - subdir: lib/amazonka - url: https://github.com/brendanhay/amazonka/archive/f73a957d05f64863e867cf39d0db260718f0fadd.tar.gz + hackage: amazonka-2.0 - completed: - name: amazonka-core + hackage: amazonka-core-2.0@sha256:d9f0533c272ac92bd7b18699077038b6b51b3552e91b65743af4ce646286b4f8,4383 pantry-tree: - sha256: 2eadbad33f65f20781409c4de9faee04e7e4baa92906db696b78689f53de0a83 - size: 3117 - sha256: 14aeaa9f748f7ac03683e8a8126760ed16aa82152404a96c0333b582444cd381 - size: 27775608 - subdir: lib/amazonka-core - url: https://github.com/brendanhay/amazonka/archive/f73a957d05f64863e867cf39d0db260718f0fadd.tar.gz - version: '2.0' + sha256: 46e7e4de910b08ee2df98db9cda2becf388ce49510024018289a46c43e175ee0 + size: 3222 original: - subdir: lib/amazonka-core - url: https://github.com/brendanhay/amazonka/archive/f73a957d05f64863e867cf39d0db260718f0fadd.tar.gz + hackage: amazonka-core-2.0 - completed: - name: amazonka-certificatemanager + hackage: amazonka-certificatemanager-2.0@sha256:9a203a46ec1eaae2c59aa891efa480f84411783d02ba973820d67e95cc67756c,5226 pantry-tree: - sha256: 86c39ebac8e40030c05048385c1153deb124193e49bb2651c38bd1232bbd3fff - size: 7063 - sha256: 14aeaa9f748f7ac03683e8a8126760ed16aa82152404a96c0333b582444cd381 - size: 27775608 - subdir: lib/services/amazonka-certificatemanager - url: https://github.com/brendanhay/amazonka/archive/f73a957d05f64863e867cf39d0db260718f0fadd.tar.gz - version: '2.0' + sha256: 9ee7f26c6166f2b01f32efcf41d4a6315ff681823c698f49036f5b471ffb6e9c + size: 7191 original: - subdir: lib/services/amazonka-certificatemanager - url: https://github.com/brendanhay/amazonka/archive/f73a957d05f64863e867cf39d0db260718f0fadd.tar.gz + hackage: amazonka-certificatemanager-2.0 - completed: - name: amazonka-cloudformation + hackage: amazonka-cloudformation-2.0@sha256:7a9618bf697cdaf0a51c2d7be557ad47820b926416d79f5138ff3befdbfcbafb,11870 pantry-tree: - sha256: a9f557fdf3f3d5f960a28921465609e338ce702e1ecdf6e559e01efdccb364a6 - size: 25784 - sha256: 14aeaa9f748f7ac03683e8a8126760ed16aa82152404a96c0333b582444cd381 - size: 27775608 - subdir: lib/services/amazonka-cloudformation - url: https://github.com/brendanhay/amazonka/archive/f73a957d05f64863e867cf39d0db260718f0fadd.tar.gz - version: '2.0' + sha256: 177fbc16ea2fa072a7fca9f4a3b1d64f4d5e8fc7cd493e4e841f337c572745bd + size: 27257 original: - subdir: lib/services/amazonka-cloudformation - url: https://github.com/brendanhay/amazonka/archive/f73a957d05f64863e867cf39d0db260718f0fadd.tar.gz + hackage: amazonka-cloudformation-2.0 - completed: - name: amazonka-ec2 + hackage: amazonka-ec2-2.0@sha256:9344b87d8f8328fd91023b96565e79e7676aa5e7dd40b87b3f3f3a22a9da7736,74154 pantry-tree: - sha256: 29c4666aa6cd81a371cdef208199f45a99d9fdef31f3ff9450c1762a64dd60d0 - size: 190150 - sha256: 14aeaa9f748f7ac03683e8a8126760ed16aa82152404a96c0333b582444cd381 - size: 27775608 - subdir: lib/services/amazonka-ec2 - url: https://github.com/brendanhay/amazonka/archive/f73a957d05f64863e867cf39d0db260718f0fadd.tar.gz - version: '2.0' + sha256: d1f2d4fce5b0664605d730d4232b25f26a5f49e3b7d07f4b282e8c36773e5ffd + size: 234434 original: - subdir: lib/services/amazonka-ec2 - url: https://github.com/brendanhay/amazonka/archive/f73a957d05f64863e867cf39d0db260718f0fadd.tar.gz + hackage: amazonka-ec2-2.0 - completed: - name: amazonka-ecr + hackage: amazonka-ecr-2.0@sha256:88ec5dffb3c07f9e49eb4d9672ac62c175b6cf2c3e044ec0e4c705cd6bff3487,6925 pantry-tree: - sha256: 8c8b2a242ac973b0205916de20d8066cf3820acd47d98d6ec6df9182f6b31966 - size: 12161 - sha256: 14aeaa9f748f7ac03683e8a8126760ed16aa82152404a96c0333b582444cd381 - size: 27775608 - subdir: lib/services/amazonka-ecr - url: https://github.com/brendanhay/amazonka/archive/f73a957d05f64863e867cf39d0db260718f0fadd.tar.gz - version: '2.0' + sha256: d0d5dc0ed4aab28f0d6183657e77985693c5ed011dd0cc40d335b6a334b1939a + size: 15627 original: - subdir: lib/services/amazonka-ecr - url: https://github.com/brendanhay/amazonka/archive/f73a957d05f64863e867cf39d0db260718f0fadd.tar.gz + hackage: amazonka-ecr-2.0 - completed: - name: amazonka-lambda + hackage: amazonka-lambda-2.0@sha256:aa74299380318b04429980eb76b7f0499a8241ff01de859042b0ff09bd7ef420,8281 pantry-tree: - sha256: 7e6feb0f8af0a9f6ce20db04d69a0e7b92838f27d17f65f0cd1a3c87b6a6331e - size: 19117 - sha256: 14aeaa9f748f7ac03683e8a8126760ed16aa82152404a96c0333b582444cd381 - size: 27775608 - subdir: lib/services/amazonka-lambda - url: https://github.com/brendanhay/amazonka/archive/f73a957d05f64863e867cf39d0db260718f0fadd.tar.gz - version: '2.0' + sha256: da8f346de9d1eb0fb12afa91e44f8179ac05176043919346e2e72a7880b7a9e5 + size: 21343 original: - subdir: lib/services/amazonka-lambda - url: https://github.com/brendanhay/amazonka/archive/f73a957d05f64863e867cf39d0db260718f0fadd.tar.gz + hackage: amazonka-lambda-2.0 - completed: - name: amazonka-sso + hackage: amazonka-sso-2.0@sha256:902be13b604e4a3b51a9b8e1adc6a32f42322ae11f738a72a8c737b2d0a91a5e,2995 pantry-tree: - sha256: f11babeeaf0481ae68134ced86e9d1d9396d1beb7bd70e0a1e6b77bc4148a192 - size: 1869 - sha256: 14aeaa9f748f7ac03683e8a8126760ed16aa82152404a96c0333b582444cd381 - size: 27775608 - subdir: lib/services/amazonka-sso - url: https://github.com/brendanhay/amazonka/archive/f73a957d05f64863e867cf39d0db260718f0fadd.tar.gz - version: '2.0' + sha256: f87dd959a78bf54295bd6f8c7da58f7f8f860251d5548ecb05ab758e03cba50b + size: 1817 original: - subdir: lib/services/amazonka-sso - url: https://github.com/brendanhay/amazonka/archive/f73a957d05f64863e867cf39d0db260718f0fadd.tar.gz + hackage: amazonka-sso-2.0 - completed: - name: amazonka-sts + hackage: amazonka-sts-2.0@sha256:5c721083e8d80883a893176de6105c27bbbd8176f467c27ac5f8d548a5e726d8,3209 pantry-tree: - sha256: 64ed22eaaea868b32cf56f162d1bd7332b048d8f2ea073c4e9827ed08e71cc70 - size: 2932 - sha256: 14aeaa9f748f7ac03683e8a8126760ed16aa82152404a96c0333b582444cd381 - size: 27775608 - subdir: lib/services/amazonka-sts - url: https://github.com/brendanhay/amazonka/archive/f73a957d05f64863e867cf39d0db260718f0fadd.tar.gz - version: '2.0' + sha256: bde4691af7cac74e0a3705271b4d3ac05515863bfb6f668112e3f3950a27cb41 + size: 2880 original: - subdir: lib/services/amazonka-sts - url: https://github.com/brendanhay/amazonka/archive/f73a957d05f64863e867cf39d0db260718f0fadd.tar.gz + hackage: amazonka-sts-2.0 +- completed: + hackage: crypton-0.33@sha256:5e92f29b9b7104d91fcdda1dec9400c9ad1f1791c231cc41ceebd783fb517dee,18202 + pantry-tree: + sha256: 38809499d7f9775ef45cd29ab5c3dc9b283a813f34c1cdc56681b24f8cf8bb4f + size: 23148 + original: + hackage: crypton-0.33 snapshots: - completed: sha256: 3770dfd79f5aed67acdcc65c4e7730adddffe6dba79ea723cfb0918356fc0f94 diff --git a/stackctl.cabal b/stackctl.cabal index b3e6bed..4acd86e 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -103,13 +103,13 @@ library , aeson , aeson-casing , aeson-pretty - , amazonka - , amazonka-cloudformation - , amazonka-core - , amazonka-ec2 - , amazonka-lambda - , amazonka-sso - , amazonka-sts + , amazonka >=2.0 + , amazonka-cloudformation >=2.0 + , amazonka-core >=2.0 + , amazonka-ec2 >=2.0 + , amazonka-lambda >=2.0 + , amazonka-sso >=2.0 + , amazonka-sts >=2.0 , base ==4.* , bytestring , cfn-flip >=0.1.0.3 From defc1b7de5ace2bde187eac6b637d5670f9ab2c7 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Mon, 28 Aug 2023 13:13:37 -0400 Subject: [PATCH 072/187] Version bump --- CHANGELOG.md | 8 +++++++- package.yaml | 2 +- stackctl.cabal | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 681e8b1..5caa541 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,10 @@ -## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.4.2.1...main) +## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.4.2.2...main) + +## [v1.4.2.2](https://github.com/freckle/stackctl/compare/v1.4.2.1...v1.4.2.2) + +- Use `amazonka-2.0` :tada: +- Finalize update to `UnliftIO.Exception.Lens` +- Re-export upstreamed `Blammo.Logging.Colors` ## [v1.4.2.1](https://github.com/freckle/stackctl/compare/v1.4.2.0...v1.4.2.1) diff --git a/package.yaml b/package.yaml index 546067e..2a4d096 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: stackctl -version: 1.4.2.1 +version: 1.4.2.2 github: freckle/stackctl license: MIT author: Freckle Engineering diff --git a/stackctl.cabal b/stackctl.cabal index 4acd86e..ae1dcb6 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -5,7 +5,7 @@ cabal-version: 1.18 -- see: https://github.com/sol/hpack name: stackctl -version: 1.4.2.1 +version: 1.4.2.2 description: Please see homepage: https://github.com/freckle/stackctl#readme bug-reports: https://github.com/freckle/stackctl/issues From 0b8026d9f8ac9530aa222b23664a9568e5bf99f8 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Fri, 30 Jun 2023 13:52:13 -0400 Subject: [PATCH 073/187] Add forEachSpec_ --- src/Stackctl/Spec/Changes.hs | 4 +--- src/Stackctl/Spec/Deploy.hs | 4 +--- src/Stackctl/Spec/Discover.hs | 17 ++++++++++++++++- src/Stackctl/Spec/List.hs | 3 +-- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/Stackctl/Spec/Changes.hs b/src/Stackctl/Spec/Changes.hs index 7ff6ebe..e3fc021 100644 --- a/src/Stackctl/Spec/Changes.hs +++ b/src/Stackctl/Spec/Changes.hs @@ -76,9 +76,7 @@ runChanges ChangesOptions {..} = do Nothing -> pushLoggerLn formatted Just p -> liftIO $ T.appendFile p $ formatted <> "\n" - specs <- discoverSpecs - - for_ specs $ \spec -> do + forEachSpec_ $ \spec -> do withThreadContext ["stackName" .= stackSpecStackName spec] $ do emChangeSet <- createChangeSet spec scoParameters scoTags diff --git a/src/Stackctl/Spec/Deploy.hs b/src/Stackctl/Spec/Deploy.hs index a9d6c2e..ffc632d 100644 --- a/src/Stackctl/Spec/Deploy.hs +++ b/src/Stackctl/Spec/Deploy.hs @@ -88,9 +88,7 @@ runDeploy DeployOptions {..} = do removed <- inferRemovedStacks traverse_ (deleteRemovedStack sdoDeployConfirmation) removed - specs <- discoverSpecs - - for_ specs $ \spec -> do + forEachSpec_ $ \spec -> do withThreadContext ["stackName" .= stackSpecStackName spec] $ do checkIfStackRequiresDeletion sdoDeployConfirmation $ stackSpecStackName spec diff --git a/src/Stackctl/Spec/Discover.hs b/src/Stackctl/Spec/Discover.hs index 6153ee8..6fddb20 100644 --- a/src/Stackctl/Spec/Discover.hs +++ b/src/Stackctl/Spec/Discover.hs @@ -1,5 +1,6 @@ module Stackctl.Spec.Discover - ( discoverSpecs + ( forEachSpec_ + , discoverSpecs , buildSpecPath ) where @@ -17,6 +18,20 @@ import Stackctl.StackSpecPath import System.FilePath (isPathSeparator) import System.FilePath.Glob +forEachSpec_ + :: ( MonadMask m + , MonadResource m + , MonadLogger m + , MonadReader env m + , HasAwsScope env + , HasConfig env + , HasDirectoryOption env + , HasFilterOption env + ) + => (StackSpec -> m ()) + -> m () +forEachSpec_ f = traverse_ f =<< discoverSpecs + discoverSpecs :: ( MonadMask m , MonadResource m diff --git a/src/Stackctl/Spec/List.hs b/src/Stackctl/Spec/List.hs index 25369fa..b12dceb 100644 --- a/src/Stackctl/Spec/List.hs +++ b/src/Stackctl/Spec/List.hs @@ -40,10 +40,9 @@ runList => ListOptions -> m () runList _ = do - specs <- discoverSpecs Colors {..} <- getColorsLogger - for_ specs $ \spec -> do + forEachSpec_ $ \spec -> do let path = stackSpecFilePath spec name = stackSpecStackName spec From 9fcf0ffc418cc95825ce1460e80bebfc1bb63e98 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Mon, 28 Aug 2023 15:49:23 -0400 Subject: [PATCH 074/187] Add awsWithAuth This function allows access to the underlying `AuthEnv`. It's not used by Stackctl (yet) but is used by tooling that uses Stackctl as its AWS SDK wrapper to supply concrete keys to external processes that aren't SSO-session aware. In the future, this AWS "library" should be extracted and shared, but for now we accept such minor extensions even if they only exist for these external. use-cases. --- src/Stackctl/AWS/Core.hs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Stackctl/AWS/Core.hs b/src/Stackctl/AWS/Core.hs index 28e1a01..73fa269 100644 --- a/src/Stackctl/AWS/Core.hs +++ b/src/Stackctl/AWS/Core.hs @@ -2,6 +2,7 @@ module Stackctl.AWS.Core ( AwsEnv , HasAwsEnv (..) , awsEnvDiscover + , awsWithAuth , awsSimple , awsSend , awsPaginate @@ -32,7 +33,7 @@ import Amazonka hiding (LogLevel (..)) import qualified Amazonka as AWS import Amazonka.Auth.Keys (fromSession) import Amazonka.Data.Text (FromText (..), ToText (..)) -import Amazonka.Env (env_logger, env_region) +import Amazonka.Env (env_auth, env_logger, env_region) import Amazonka.STS.AssumeRole import Conduit (ConduitM) import Control.Monad.Logger (defaultLoc, toLogStr) @@ -74,6 +75,12 @@ class HasAwsEnv env where instance HasAwsEnv AwsEnv where awsEnvL = id +awsWithAuth + :: (MonadIO m, MonadReader env m, HasAwsEnv env) => (AuthEnv -> m a) -> m a +awsWithAuth f = do + auth <- view $ awsEnvL . unL . env_auth . to runIdentity + withAuth auth f + awsSimple :: ( MonadResource m , MonadReader env m From 9c0894d03057bf7509b83e1950a9865fba9e6d8e Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Mon, 28 Aug 2023 15:52:58 -0400 Subject: [PATCH 075/187] Version bump --- CHANGELOG.md | 7 ++++++- package.yaml | 2 +- stackctl.cabal | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5caa541..11cafce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ -## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.4.2.2...main) +## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.4.3.0...main) + +## [v1.4.3.0](https://github.com/freckle/stackctl/compare/v1.4.2.2...v1.4.3.0) + +- Add `awsWithAuth` +- Add `forEachSpec_` ## [v1.4.2.2](https://github.com/freckle/stackctl/compare/v1.4.2.1...v1.4.2.2) diff --git a/package.yaml b/package.yaml index 2a4d096..23e607c 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: stackctl -version: 1.4.2.2 +version: 1.4.3.0 github: freckle/stackctl license: MIT author: Freckle Engineering diff --git a/stackctl.cabal b/stackctl.cabal index ae1dcb6..7ab2b95 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -5,7 +5,7 @@ cabal-version: 1.18 -- see: https://github.com/sol/hpack name: stackctl -version: 1.4.2.2 +version: 1.4.3.0 description: Please see homepage: https://github.com/freckle/stackctl#readme bug-reports: https://github.com/freckle/stackctl/issues From e3202cedea7d6ee820c371063d4aba73d1d40f91 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 29 Aug 2023 17:04:55 -0400 Subject: [PATCH 076/187] Add awsSilently This wrapper modifies the underlying `Env` to not log. This is useful to avoid log noise when errors are expected (such as this not-found check), since amazonka-2.0 logs certain errors that it didn't before, in addition to throwing them. --- src/Stackctl/AWS/CloudFormation.hs | 6 ++++-- src/Stackctl/AWS/Core.hs | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Stackctl/AWS/CloudFormation.hs b/src/Stackctl/AWS/CloudFormation.hs index 283f189..9698973 100644 --- a/src/Stackctl/AWS/CloudFormation.hs +++ b/src/Stackctl/AWS/CloudFormation.hs @@ -185,8 +185,10 @@ awsCloudFormationDescribeStackMaybe awsCloudFormationDescribeStackMaybe stackName = -- AWS gives us a 400 if the stackName doesn't exist, rather than simply -- returning an empty list, so we need to do this through exceptions - handling_ _ValidationError (pure Nothing) $ do - Just <$> awsCloudFormationDescribeStack stackName + handling_ _ValidationError (pure Nothing) + $ awsSilently -- don't log said 400 + $ Just + <$> awsCloudFormationDescribeStack stackName awsCloudFormationDescribeStackOutputs :: (MonadResource m, MonadReader env m, HasAwsEnv env) diff --git a/src/Stackctl/AWS/Core.hs b/src/Stackctl/AWS/Core.hs index 73fa269..4b9db56 100644 --- a/src/Stackctl/AWS/Core.hs +++ b/src/Stackctl/AWS/Core.hs @@ -12,6 +12,7 @@ module Stackctl.AWS.Core -- * Modifiers on 'AwsEnv' , awsWithin , awsTimeout + , awsSilently -- * 'Amazonka' extensions , AccountId (..) @@ -174,6 +175,11 @@ awsWithin r = local $ awsEnvL . unL . env_region .~ r awsTimeout :: (MonadReader env m, HasAwsEnv env) => Seconds -> m a -> m a awsTimeout t = local $ over (awsEnvL . unL) (globalTimeout t) +awsSilently :: (MonadReader env m, HasAwsEnv env) => m a -> m a +awsSilently = local $ awsEnvL . unL . env_logger .~ noop + where + noop _level _msg = pure () + newtype AccountId = AccountId { unAccountId :: Text } From 2ec0b1ae33c9d89350d64e5bc2fe20455dc8ce9f Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 29 Aug 2023 17:06:27 -0400 Subject: [PATCH 077/187] Remove another re-implementation of handling This site still used `handleJust`, with its complicated type annotation, from back before `UnliftIO.Exception.Lens` existed. --- src/Stackctl/AWS/Core.hs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Stackctl/AWS/Core.hs b/src/Stackctl/AWS/Core.hs index 4b9db56..47b8c37 100644 --- a/src/Stackctl/AWS/Core.hs +++ b/src/Stackctl/AWS/Core.hs @@ -40,6 +40,7 @@ import Conduit (ConduitM) import Control.Monad.Logger (defaultLoc, toLogStr) import Control.Monad.Trans.Resource (MonadResource) import Stackctl.AWS.Orphans () +import UnliftIO.Exception.Lens (handling) newtype AwsEnv = AwsEnv { unAwsEnv :: Env @@ -191,7 +192,7 @@ newtype AccountId = AccountId -- makes things more readable and easier to debug. handlingServiceError :: (MonadUnliftIO m, MonadLogger m) => m a -> m a handlingServiceError = - handleJust @_ @SomeException (^? _ServiceError) $ \e -> do + handling _ServiceError $ \e -> do logError $ "Exiting due to AWS Service error" :# [ "code" .= toText (e ^. serviceError_code) From f504dcdddf73a328851817783b9f598ef76216e8 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 29 Aug 2023 17:07:41 -0400 Subject: [PATCH 078/187] Version bump --- CHANGELOG.md | 6 +++++- package.yaml | 2 +- stackctl.cabal | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11cafce..890af36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ -## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.4.3.0...main) +## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.4.4.0...main) + +## [v1.4.4.0](https://github.com/freckle/stackctl/compare/v1.4.3.0...v1.4.4.0) + +- Add `awsSilently` ## [v1.4.3.0](https://github.com/freckle/stackctl/compare/v1.4.2.2...v1.4.3.0) diff --git a/package.yaml b/package.yaml index 23e607c..86b65fb 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: stackctl -version: 1.4.3.0 +version: 1.4.4.0 github: freckle/stackctl license: MIT author: Freckle Engineering diff --git a/stackctl.cabal b/stackctl.cabal index 7ab2b95..f6551b4 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -5,7 +5,7 @@ cabal-version: 1.18 -- see: https://github.com/sol/hpack name: stackctl -version: 1.4.3.0 +version: 1.4.4.0 description: Please see homepage: https://github.com/freckle/stackctl#readme bug-reports: https://github.com/freckle/stackctl/issues From a4ade001fd5bf5bb7a6b0538f2ed42319e9a584a Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 29 Aug 2023 17:12:48 -0400 Subject: [PATCH 079/187] Use lens operator more consistently --- src/Stackctl/AWS/Core.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Stackctl/AWS/Core.hs b/src/Stackctl/AWS/Core.hs index 47b8c37..55424c9 100644 --- a/src/Stackctl/AWS/Core.hs +++ b/src/Stackctl/AWS/Core.hs @@ -174,7 +174,7 @@ awsWithin :: (MonadReader env m, HasAwsEnv env) => Region -> m a -> m a awsWithin r = local $ awsEnvL . unL . env_region .~ r awsTimeout :: (MonadReader env m, HasAwsEnv env) => Seconds -> m a -> m a -awsTimeout t = local $ over (awsEnvL . unL) (globalTimeout t) +awsTimeout t = local $ awsEnvL . unL %~ globalTimeout t awsSilently :: (MonadReader env m, HasAwsEnv env) => m a -> m a awsSilently = local $ awsEnvL . unL . env_logger .~ noop From 4e77451eff64c88c7818de5e87834bdbafb0a517 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 30 Aug 2023 09:05:51 -0400 Subject: [PATCH 080/187] Improve status indicators in stackctl-ls - Add more granularity A Stack that is not yet deployed at all may look deployed because it exists once the first change set is in review. Having a separate indication for this is valuable -- we might as well make more too. - Print a legend at the end Now that we have more complicated indicators, showing what they mean seems valuable. A `--no-legend` option was added to suppress this, in case the output is being parsed line-wise. --- man/stackctl-ls.1.ronn | 9 ++-- src/Stackctl/AWS/CloudFormation.hs | 1 + src/Stackctl/Spec/List.hs | 79 +++++++++++++++++++++++++++--- 3 files changed, 77 insertions(+), 12 deletions(-) diff --git a/man/stackctl-ls.1.ronn b/man/stackctl-ls.1.ronn index 92472cb..2bd1a65 100644 --- a/man/stackctl-ls.1.ronn +++ b/man/stackctl-ls.1.ronn @@ -3,7 +3,7 @@ stackctl-ls(1) - list stack specifications ## SYNOPSIS -`stackctl ls` +`stackctl ls` [] ## DESCRIPTION @@ -11,9 +11,10 @@ This command locates `stacks/` for the currently-authorized AWS Account and Region and lists them. The key differences between this and stackctl-cat(1) is that this command lists -things as simple rows and indicates for each spec if a deployed stack exists in -the first column. +things as simple rows and indicates for each spec the state of the stack in the +first column. ## OPTIONS -None. + * `--no-legend`: + Don't print indicators legend at the end. diff --git a/src/Stackctl/AWS/CloudFormation.hs b/src/Stackctl/AWS/CloudFormation.hs index 9698973..ad47c5f 100644 --- a/src/Stackctl/AWS/CloudFormation.hs +++ b/src/Stackctl/AWS/CloudFormation.hs @@ -1,6 +1,7 @@ module Stackctl.AWS.CloudFormation ( Stack (..) , stack_stackName + , stack_stackStatus , stackDescription , stackStatusRequiresDeletion , StackId (..) diff --git a/src/Stackctl/Spec/List.hs b/src/Stackctl/Spec/List.hs index b12dceb..fcd9a9c 100644 --- a/src/Stackctl/Spec/List.hs +++ b/src/Stackctl/Spec/List.hs @@ -7,6 +7,7 @@ module Stackctl.Spec.List import Stackctl.Prelude import Blammo.Logging.Logger (pushLoggerLn) +import qualified Data.Text as T import Options.Applicative import Stackctl.AWS import Stackctl.AWS.Scope @@ -17,12 +18,21 @@ import Stackctl.FilterOption (HasFilterOption) import Stackctl.Spec.Discover import Stackctl.StackSpec -data ListOptions = ListOptions - --- brittany-disable-next-binding +newtype ListOptions = ListOptions + { loLegend :: Bool + } parseListOptions :: Parser ListOptions -parseListOptions = pure ListOptions +parseListOptions = + ListOptions + <$> ( not + <$> switch + ( mconcat + [ long "no-legend" + , help "Don't print indicators legend at the end" + ] + ) + ) runList :: ( MonadUnliftIO m @@ -39,23 +49,76 @@ runList ) => ListOptions -> m () -runList _ = do - Colors {..} <- getColorsLogger +runList ListOptions {..} = do + colors@Colors {..} <- getColorsLogger forEachSpec_ $ \spec -> do let path = stackSpecFilePath spec name = stackSpecStackName spec - exists <- isJust <$> awsCloudFormationDescribeStackMaybe name + mStackStatus <- + fmap (^. stack_stackStatus) + <$> awsCloudFormationDescribeStackMaybe name + + -- logInfo $ "" :# ["stackStatus" .= mStackStatus] let + indicator = maybe NotDeployed statusIndicator mStackStatus + formatted :: Text formatted = " " - <> (if exists then green "✓ " else yellow "✗ ") + <> indicatorIcon colors indicator + <> " " <> cyan (unStackName name) <> " => " <> magenta (pack path) pushLoggerLn formatted + + let legendItem i = indicatorIcon colors i <> " " <> indicatorDescription i + + when loLegend + $ pushLoggerLn + $ "\nLegend:\n " + <> T.intercalate ", " (map legendItem [minBound .. maxBound]) + +data Indicator + = Deployed + | DeployFailed + | NotDeployed + | Reviewing + | Deploying + | Unknown + deriving stock (Bounded, Enum) + +indicatorIcon :: Colors -> Indicator -> Text +indicatorIcon Colors {..} = \case + Deployed -> green "✓" + DeployFailed -> red "✗" + NotDeployed -> yellow "_" + Reviewing -> yellow "∇" + Deploying -> cyan "⋅" + Unknown -> magenta "?" + +indicatorDescription :: Indicator -> Text +indicatorDescription = \case + Deployed -> "deployed" + DeployFailed -> "failed or rolled back" + NotDeployed -> "doesn't exist" + Reviewing -> "reviewing" + Deploying -> "deploying" + Unknown -> "unknown" + +statusIndicator :: StackStatus -> Indicator +statusIndicator = \case + StackStatus_REVIEW_IN_PROGRESS -> Reviewing + StackStatus_ROLLBACK_COMPLETE -> DeployFailed + x | statusSuffixed "_IN_PROGRESS" x -> Deploying + x | statusSuffixed "_FAILED" x -> DeployFailed + x | statusSuffixed "_ROLLBACK_COMPLETE" x -> DeployFailed + x | statusSuffixed "_COMPLETE" x -> Deployed + _ -> Unknown + where + statusSuffixed x = (x `T.isSuffixOf`) . fromStackStatus From 75504b9b0bc190b5b1f01bce7a034f9ad7e2c510 Mon Sep 17 00:00:00 2001 From: Pat Brisbin Date: Wed, 30 Aug 2023 14:58:30 -0400 Subject: [PATCH 081/187] Update src/Stackctl/Spec/List.hs --- src/Stackctl/Spec/List.hs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Stackctl/Spec/List.hs b/src/Stackctl/Spec/List.hs index fcd9a9c..f02355d 100644 --- a/src/Stackctl/Spec/List.hs +++ b/src/Stackctl/Spec/List.hs @@ -61,8 +61,6 @@ runList ListOptions {..} = do fmap (^. stack_stackStatus) <$> awsCloudFormationDescribeStackMaybe name - -- logInfo $ "" :# ["stackStatus" .= mStackStatus] - let indicator = maybe NotDeployed statusIndicator mStackStatus From bd900241c4870949bf54c8f5c5b18e5271e17cb2 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 1 Aug 2023 15:20:04 -0400 Subject: [PATCH 082/187] Add golden specs on formatChangeSet Use [hspec-golden][] to assert formatting of example changesets (so far just one) doesn't change unexpectedly. This will allow safe refactoring and extension of this content without having to generate actual changesets and/or GitHub comments to test. Approach: every `.json` file in the given directory is formatted and the result compared against the equivalent `.txt` file (for `--format=tty`) or a `.md` file (for `--format=pr`). [hspec-golden]: https://hackage.haskell.org/package/hspec-golden-0.2.1.0 --- package.yaml | 3 + src/Stackctl/AWS/CloudFormation.hs | 34 +- src/Stackctl/AWS/Orphans.hs | 71 +- src/Stackctl/Spec/Changes/Format.hs | 1 + stack.yaml | 2 + stack.yaml.lock | 7 + stackctl.cabal | 6 +- test/Stackctl/Spec/Changes/FormatSpec.hs | 48 + test/files/change-sets/prod-faktory.json | 1114 ++++++++++++++++++++++ test/files/change-sets/prod-faktory.md | 27 + test/files/change-sets/prod-faktory.txt | 130 +++ 11 files changed, 1387 insertions(+), 56 deletions(-) create mode 100644 test/Stackctl/Spec/Changes/FormatSpec.hs create mode 100644 test/files/change-sets/prod-faktory.json create mode 100644 test/files/change-sets/prod-faktory.md create mode 100644 test/files/change-sets/prod-faktory.txt diff --git a/package.yaml b/package.yaml index 86b65fb..3e9eef0 100644 --- a/package.yaml +++ b/package.yaml @@ -113,10 +113,13 @@ tests: main: Spec.hs source-dirs: test dependencies: + - Glob - QuickCheck - aeson - bytestring + - filepath - hspec + - hspec-golden >= 0.2.1.0 - mtl - stackctl - yaml diff --git a/src/Stackctl/AWS/CloudFormation.hs b/src/Stackctl/AWS/CloudFormation.hs index ad47c5f..b764ef6 100644 --- a/src/Stackctl/AWS/CloudFormation.hs +++ b/src/Stackctl/AWS/CloudFormation.hs @@ -46,6 +46,7 @@ module Stackctl.AWS.CloudFormation -- * ChangeSets , ChangeSet (..) + , changeSetFromResponse , changeSetJSON , ChangeSetId (..) , ChangeSetName (..) @@ -340,6 +341,23 @@ data ChangeSet = ChangeSet , csResponse :: DescribeChangeSetResponse } +changeSetFromResponse :: DescribeChangeSetResponse -> Maybe ChangeSet +changeSetFromResponse resp = + ChangeSet + <$> (resp ^. describeChangeSetResponse_creationTime) + <*> pure (fmap sortChanges $ resp ^. describeChangeSetResponse_changes) + <*> (ChangeSetName <$> resp ^. describeChangeSetResponse_changeSetName) + <*> (resp ^. describeChangeSetResponse_executionStatus) + <*> (ChangeSetId <$> resp ^. describeChangeSetResponse_changeSetId) + <*> pure (resp ^. describeChangeSetResponse_parameters) + <*> (StackId <$> resp ^. describeChangeSetResponse_stackId) + <*> pure (resp ^. describeChangeSetResponse_capabilities) + <*> pure (resp ^. describeChangeSetResponse_tags) + <*> (StackName <$> resp ^. describeChangeSetResponse_stackName) + <*> pure (resp ^. describeChangeSetResponse_status) + <*> pure (resp ^. describeChangeSetResponse_statusReason) + <*> pure resp + changeSetJSON :: ChangeSet -> Text changeSetJSON = decodeUtf8 . BSL.toStrict . encodePretty . csResponse @@ -406,21 +424,7 @@ awsCloudFormationDescribeChangeSet -> m ChangeSet awsCloudFormationDescribeChangeSet changeSetId = do let req = newDescribeChangeSet $ unChangeSetId changeSetId - awsSimple "DescribeChangeSet" req $ \resp -> - ChangeSet - <$> (resp ^. describeChangeSetResponse_creationTime) - <*> pure (fmap sortChanges $ resp ^. describeChangeSetResponse_changes) - <*> (ChangeSetName <$> resp ^. describeChangeSetResponse_changeSetName) - <*> (resp ^. describeChangeSetResponse_executionStatus) - <*> (ChangeSetId <$> resp ^. describeChangeSetResponse_changeSetId) - <*> pure (resp ^. describeChangeSetResponse_parameters) - <*> (StackId <$> resp ^. describeChangeSetResponse_stackId) - <*> pure (resp ^. describeChangeSetResponse_capabilities) - <*> pure (resp ^. describeChangeSetResponse_tags) - <*> (StackName <$> resp ^. describeChangeSetResponse_stackName) - <*> pure (resp ^. describeChangeSetResponse_status) - <*> pure (resp ^. describeChangeSetResponse_statusReason) - <*> pure resp + awsSimple "DescribeChangeSet" req changeSetFromResponse sortChanges :: [Change] -> [Change] sortChanges = sortByDependencies changeName changeCausedBy diff --git a/src/Stackctl/AWS/Orphans.hs b/src/Stackctl/AWS/Orphans.hs index a1867f7..00658c0 100644 --- a/src/Stackctl/AWS/Orphans.hs +++ b/src/Stackctl/AWS/Orphans.hs @@ -16,6 +16,15 @@ import GHC.Generics (Rep) -- Makes it syntactally easier to do a bunch of these newtype Generically a = Generically {unGenerically :: a} + +instance + ( Generic a + , GFromJSON Zero (Rep a) + ) + => FromJSON (Generically a) + where + parseJSON = fmap Generically . genericParseJSON defaultOptions + instance ( Generic a , GToJSON' Value Zero (Rep a) @@ -26,43 +35,25 @@ instance toJSON = genericToJSON defaultOptions . unGenerically toEncoding = genericToEncoding defaultOptions . unGenerically -deriving via - (Generically DescribeChangeSetResponse) - instance - ToJSON DescribeChangeSetResponse -deriving via - (Generically Tag) - instance - ToJSON Tag -deriving via - (Generically Parameter) - instance - ToJSON Parameter -deriving via - (Generically RollbackConfiguration) - instance - ToJSON RollbackConfiguration -deriving via - (Generically RollbackTrigger) - instance - ToJSON RollbackTrigger -deriving via - (Generically Change) - instance - ToJSON Change -deriving via - (Generically ResourceChange) - instance - ToJSON ResourceChange -deriving via - (Generically ModuleInfo) - instance - ToJSON ModuleInfo -deriving via - (Generically ResourceChangeDetail) - instance - ToJSON ResourceChangeDetail -deriving via - (Generically ResourceTargetDefinition) - instance - ToJSON ResourceTargetDefinition +{- FOURMOLU_DISABLE -} + +deriving via (Generically Change) instance FromJSON Change +deriving via (Generically Change) instance ToJSON Change +deriving via (Generically DescribeChangeSetResponse) instance FromJSON DescribeChangeSetResponse +deriving via (Generically DescribeChangeSetResponse) instance ToJSON DescribeChangeSetResponse +deriving via (Generically ModuleInfo) instance FromJSON ModuleInfo +deriving via (Generically ModuleInfo) instance ToJSON ModuleInfo +deriving via (Generically Parameter) instance FromJSON Parameter +deriving via (Generically Parameter) instance ToJSON Parameter +deriving via (Generically ResourceChange) instance FromJSON ResourceChange +deriving via (Generically ResourceChange) instance ToJSON ResourceChange +deriving via (Generically ResourceChangeDetail) instance FromJSON ResourceChangeDetail +deriving via (Generically ResourceChangeDetail) instance ToJSON ResourceChangeDetail +deriving via (Generically ResourceTargetDefinition) instance FromJSON ResourceTargetDefinition +deriving via (Generically ResourceTargetDefinition) instance ToJSON ResourceTargetDefinition +deriving via (Generically RollbackConfiguration) instance FromJSON RollbackConfiguration +deriving via (Generically RollbackConfiguration) instance ToJSON RollbackConfiguration +deriving via (Generically RollbackTrigger) instance FromJSON RollbackTrigger +deriving via (Generically RollbackTrigger) instance ToJSON RollbackTrigger +deriving via (Generically Tag) instance FromJSON Tag +deriving via (Generically Tag) instance ToJSON Tag diff --git a/src/Stackctl/Spec/Changes/Format.hs b/src/Stackctl/Spec/Changes/Format.hs index 0ba7b16..ddb5291 100644 --- a/src/Stackctl/Spec/Changes/Format.hs +++ b/src/Stackctl/Spec/Changes/Format.hs @@ -19,6 +19,7 @@ import Stackctl.Colors data Format = FormatTTY | FormatPullRequest + deriving stock (Bounded, Enum, Show) data OmitFull = OmitFull diff --git a/stack.yaml b/stack.yaml index 3a4757c..2511a0d 100644 --- a/stack.yaml +++ b/stack.yaml @@ -15,5 +15,7 @@ extra-deps: - amazonka-sso-2.0 - amazonka-sts-2.0 + - hspec-golden-0.2.1.0 + # For amazonka-core-2.0 - crypton-0.33 diff --git a/stack.yaml.lock b/stack.yaml.lock index 7659034..a088c0b 100644 --- a/stack.yaml.lock +++ b/stack.yaml.lock @@ -88,6 +88,13 @@ packages: size: 2880 original: hackage: amazonka-sts-2.0 +- completed: + hackage: hspec-golden-0.2.1.0@sha256:b695ae72685bbb5acd04cdd79d07c43de5ab8867e28662dd1a0002296f2a4940,2635 + pantry-tree: + sha256: d72fec5f2c0568ae958282c7a8b8f5bfba146e3e4ceee0510c0e22be5c8eb740 + size: 495 + original: + hackage: hspec-golden-0.2.1.0 - completed: hackage: crypton-0.33@sha256:5e92f29b9b7104d91fcdda1dec9400c9ad1f1791c231cc41ceebd783fb517dee,18202 pantry-tree: diff --git a/stackctl.cabal b/stackctl.cabal index f6551b4..7d730e3 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -185,6 +185,7 @@ test-suite spec Stackctl.Config.RequiredVersionSpec Stackctl.ConfigSpec Stackctl.FilterOptionSpec + Stackctl.Spec.Changes.FormatSpec Stackctl.StackDescriptionSpec Stackctl.StackSpecSpec Stackctl.StackSpecYamlSpec @@ -219,11 +220,14 @@ test-suite spec TypeFamilies ghc-options: -fignore-optim-changes -fwrite-ide-info -Weverything -Wno-all-missed-specialisations -Wno-missing-import-lists -Wno-missing-kind-signatures -Wno-missing-local-signatures -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module -Wno-unsafe -optP-Wno-nonportable-include-path build-depends: - QuickCheck + Glob + , QuickCheck , aeson , base ==4.* , bytestring + , filepath , hspec + , hspec-golden >=0.2.1.0 , mtl , stackctl , yaml diff --git a/test/Stackctl/Spec/Changes/FormatSpec.hs b/test/Stackctl/Spec/Changes/FormatSpec.hs new file mode 100644 index 0000000..e04550d --- /dev/null +++ b/test/Stackctl/Spec/Changes/FormatSpec.hs @@ -0,0 +1,48 @@ +module Stackctl.Spec.Changes.FormatSpec + ( spec + ) +where + +import Stackctl.Prelude + +import Data.Aeson +import Stackctl.AWS.CloudFormation (changeSetFromResponse) +import Stackctl.Colors +import Stackctl.Spec.Changes.Format +import System.FilePath ((-<.>)) +import System.FilePath.Glob (globDir1) +import Test.Hspec +import Test.Hspec.Golden + +spec :: Spec +spec = do + describe "formatChangeSet" $ do + paths <- runIO $ globDir1 "**/*.json" "test/files/change-sets" + + for_ paths $ \path -> do + for_ [minBound .. maxBound] $ \fmt -> do + it (path <> " as " <> show fmt) $ do + formatChangeSetGolden path fmt + +formatChangeSetGolden :: FilePath -> Format -> IO (Golden Text) +formatChangeSetGolden path fmt = do + actual <- + formatChangeSet noColors OmitFull "some-stack" fmt + . (changeSetFromResponse <=< decodeStrict) + . encodeUtf8 + <$> readFileUtf8 path + + pure + $ Golden + { output = actual + , encodePretty = unpack + , writeToFile = writeFileUtf8 + , readFromFile = readFileUtf8 + , goldenFile = path -<.> ext + , actualFile = Nothing + , failFirstTime = False + } + where + ext = case fmt of + FormatTTY -> "txt" + FormatPullRequest -> "md" diff --git a/test/files/change-sets/prod-faktory.json b/test/files/change-sets/prod-faktory.json new file mode 100644 index 0000000..25f319f --- /dev/null +++ b/test/files/change-sets/prod-faktory.json @@ -0,0 +1,1114 @@ +{ + "capabilities": [ + "CAPABILITY_IAM" + ], + "changeSetId": "arn:aws:cloudformation:us-east-1:999999999999:changeSet/stackctl-202307312011-484b4202-2b16-44d5-ad51-673ec8ec057e/87b2b49a-5bac-4202-ba2f-25e2a4f18315", + "changeSetName": "stackctl-202307312011-484b4202-2b16-44d5-ad51-673ec8ec057e", + "changes": [ + { + "resourceChange": { + "action": "Add", + "changeSetId": null, + "details": null, + "logicalResourceId": "ASGDepends", + "moduleInfo": null, + "physicalResourceId": null, + "replacement": null, + "resourceType": "AWS::CloudFormation::WaitConditionHandle", + "scope": null + }, + "type'": "Resource" + }, + { + "resourceChange": { + "action": "Modify", + "changeSetId": null, + "details": [ + { + "causingEntity": null, + "changeSource": null, + "evaluation": "Static", + "target": { + "attribute": "Tags", + "name": null, + "requiresRecreation": "Never" + } + } + ], + "logicalResourceId": "ASGLambdaExecutionRole", + "moduleInfo": null, + "physicalResourceId": "prod-faktory-ecs-cluster-ASGLambdaExecutionRole-U9NPRW10WHGZ", + "replacement": "False", + "resourceType": "AWS::IAM::Role", + "scope": [ + "Tags" + ] + }, + "type'": "Resource" + }, + { + "resourceChange": { + "action": "Modify", + "changeSetId": null, + "details": [ + { + "causingEntity": null, + "changeSource": "DirectModification", + "evaluation": "Dynamic", + "target": { + "attribute": "Properties", + "name": "FunctionName", + "requiresRecreation": "Always" + } + }, + { + "causingEntity": null, + "changeSource": null, + "evaluation": "Static", + "target": { + "attribute": "Tags", + "name": null, + "requiresRecreation": "Never" + } + }, + { + "causingEntity": "ASGLifecycleLambdaFunction.Arn", + "changeSource": "ResourceAttribute", + "evaluation": "Dynamic", + "target": { + "attribute": "Properties", + "name": "FunctionName", + "requiresRecreation": "Always" + } + } + ], + "logicalResourceId": "ASGLambdaInvokePermission", + "moduleInfo": null, + "physicalResourceId": "prod-faktory-ecs-cluster-ASGLambdaInvokePermission-1475JOG38K6P", + "replacement": "Conditional", + "resourceType": "AWS::Lambda::Permission", + "scope": [ + "Properties", + "Tags" + ] + }, + "type'": "Resource" + }, + { + "resourceChange": { + "action": "Modify", + "changeSetId": null, + "details": [ + { + "causingEntity": "ASGLifecycleLambdaFunction.Arn", + "changeSource": "ResourceAttribute", + "evaluation": "Dynamic", + "target": { + "attribute": "Properties", + "name": "Endpoint", + "requiresRecreation": "Always" + } + }, + { + "causingEntity": null, + "changeSource": "DirectModification", + "evaluation": "Dynamic", + "target": { + "attribute": "Properties", + "name": "Endpoint", + "requiresRecreation": "Always" + } + } + ], + "logicalResourceId": "ASGLambdaSNSSubscription", + "moduleInfo": null, + "physicalResourceId": "arn:aws:sns:us-east-1:999999999999:prod-faktory-ecs-cluster-ASGSNSTopic-1GZCULO2RMPPZ:4fec6f06-f72b-488b-9d97-c7a4621be7ca", + "replacement": "Conditional", + "resourceType": "AWS::SNS::Subscription", + "scope": [ + "Properties" + ] + }, + "type'": "Resource" + }, + { + "resourceChange": { + "action": "Modify", + "changeSetId": null, + "details": [ + { + "causingEntity": null, + "changeSource": null, + "evaluation": "Static", + "target": { + "attribute": "Tags", + "name": null, + "requiresRecreation": "Never" + } + }, + { + "causingEntity": null, + "changeSource": "DirectModification", + "evaluation": "Dynamic", + "target": { + "attribute": "Properties", + "name": "Role", + "requiresRecreation": "Never" + } + }, + { + "causingEntity": "ASGLambdaExecutionRole.Arn", + "changeSource": "ResourceAttribute", + "evaluation": "Dynamic", + "target": { + "attribute": "Properties", + "name": "Role", + "requiresRecreation": "Never" + } + } + ], + "logicalResourceId": "ASGLifecycleLambdaFunction", + "moduleInfo": null, + "physicalResourceId": "prod-faktory-ecs-cluster-ASGLifecycleLambdaFunctio-C1RAZ4U57W7R", + "replacement": "False", + "resourceType": "AWS::Lambda::Function", + "scope": [ + "Properties", + "Tags" + ] + }, + "type'": "Resource" + }, + { + "resourceChange": { + "action": "Modify", + "changeSetId": null, + "details": [ + { + "causingEntity": "ASGLifecycleLambdaFunction.Arn", + "changeSource": "ResourceAttribute", + "evaluation": "Dynamic", + "target": { + "attribute": "Properties", + "name": "Subscription", + "requiresRecreation": "Never" + } + }, + { + "causingEntity": null, + "changeSource": null, + "evaluation": "Static", + "target": { + "attribute": "Tags", + "name": null, + "requiresRecreation": "Never" + } + }, + { + "causingEntity": null, + "changeSource": "DirectModification", + "evaluation": "Dynamic", + "target": { + "attribute": "Properties", + "name": "Subscription", + "requiresRecreation": "Never" + } + } + ], + "logicalResourceId": "ASGSNSTopic", + "moduleInfo": null, + "physicalResourceId": "arn:aws:sns:us-east-1:999999999999:prod-faktory-ecs-cluster-ASGSNSTopic-1GZCULO2RMPPZ", + "replacement": "False", + "resourceType": "AWS::SNS::Topic", + "scope": [ + "Properties", + "Tags" + ] + }, + "type'": "Resource" + }, + { + "resourceChange": { + "action": "Modify", + "changeSetId": null, + "details": [ + { + "causingEntity": null, + "changeSource": null, + "evaluation": "Static", + "target": { + "attribute": "Tags", + "name": null, + "requiresRecreation": "Never" + } + }, + { + "causingEntity": "SNSLambdaRole.Arn", + "changeSource": "ResourceAttribute", + "evaluation": "Dynamic", + "target": { + "attribute": "Properties", + "name": "RoleARN", + "requiresRecreation": "Never" + } + }, + { + "causingEntity": null, + "changeSource": "DirectModification", + "evaluation": "Dynamic", + "target": { + "attribute": "Properties", + "name": "RoleARN", + "requiresRecreation": "Never" + } + }, + { + "causingEntity": "AutoScalingGroup", + "changeSource": "ResourceReference", + "evaluation": "Dynamic", + "target": { + "attribute": "Properties", + "name": "AutoScalingGroupName", + "requiresRecreation": "Always" + } + } + ], + "logicalResourceId": "ASGTerminateHook", + "moduleInfo": null, + "physicalResourceId": "prod-faktory-ecs-cluster-ASGTerminateHook-18CDA25XDPELR", + "replacement": "Conditional", + "resourceType": "AWS::AutoScaling::LifecycleHook", + "scope": [ + "Properties", + "Tags" + ] + }, + "type'": "Resource" + }, + { + "resourceChange": { + "action": "Modify", + "changeSetId": null, + "details": [ + { + "causingEntity": "LaunchConfiguration", + "changeSource": "ResourceReference", + "evaluation": "Static", + "target": { + "attribute": "Properties", + "name": "LaunchConfigurationName", + "requiresRecreation": "Conditionally" + } + }, + { + "causingEntity": "NLBTargetGroup", + "changeSource": "ResourceReference", + "evaluation": "Static", + "target": { + "attribute": "Properties", + "name": "TargetGroupARNs", + "requiresRecreation": "Never" + } + }, + { + "causingEntity": null, + "changeSource": "DirectModification", + "evaluation": "Static", + "target": { + "attribute": "Properties", + "name": "VPCZoneIdentifier", + "requiresRecreation": "Conditionally" + } + }, + { + "causingEntity": "Environment", + "changeSource": "ParameterReference", + "evaluation": "Static", + "target": { + "attribute": "Tags", + "name": null, + "requiresRecreation": "Never" + } + }, + { + "causingEntity": null, + "changeSource": "DirectModification", + "evaluation": "Dynamic", + "target": { + "attribute": "Tags", + "name": null, + "requiresRecreation": "Never" + } + }, + { + "causingEntity": null, + "changeSource": null, + "evaluation": "Static", + "target": { + "attribute": "Tags", + "name": null, + "requiresRecreation": "Never" + } + } + ], + "logicalResourceId": "AutoScalingGroup", + "moduleInfo": null, + "physicalResourceId": "prod-faktory-ecs-cluster-AutoScalingGroup-13NOD2PK314EV", + "replacement": "Conditional", + "resourceType": "AWS::AutoScaling::AutoScalingGroup", + "scope": [ + "Properties", + "Tags" + ] + }, + "type'": "Resource" + }, + { + "resourceChange": { + "action": "Modify", + "changeSetId": null, + "details": [ + { + "causingEntity": null, + "changeSource": "DirectModification", + "evaluation": "Dynamic", + "target": { + "attribute": "Properties", + "name": "GroupDescription", + "requiresRecreation": "Always" + } + }, + { + "causingEntity": null, + "changeSource": "DirectModification", + "evaluation": "Dynamic", + "target": { + "attribute": "Properties", + "name": "SecurityGroupIngress", + "requiresRecreation": "Never" + } + }, + { + "causingEntity": "IngressTo", + "changeSource": "ParameterReference", + "evaluation": "Static", + "target": { + "attribute": "Properties", + "name": "SecurityGroupIngress", + "requiresRecreation": "Never" + } + }, + { + "causingEntity": null, + "changeSource": null, + "evaluation": "Static", + "target": { + "attribute": "Tags", + "name": null, + "requiresRecreation": "Never" + } + }, + { + "causingEntity": null, + "changeSource": "DirectModification", + "evaluation": "Static", + "target": { + "attribute": "Properties", + "name": "VpcId", + "requiresRecreation": "Always" + } + }, + { + "causingEntity": "Name", + "changeSource": "ParameterReference", + "evaluation": "Static", + "target": { + "attribute": "Properties", + "name": "GroupDescription", + "requiresRecreation": "Always" + } + }, + { + "causingEntity": "IngressFrom", + "changeSource": "ParameterReference", + "evaluation": "Static", + "target": { + "attribute": "Properties", + "name": "SecurityGroupIngress", + "requiresRecreation": "Never" + } + } + ], + "logicalResourceId": "ClusterSecurityGroup", + "moduleInfo": null, + "physicalResourceId": "sg-04a524560191c2e42", + "replacement": "True", + "resourceType": "AWS::EC2::SecurityGroup", + "scope": [ + "Properties", + "Tags" + ] + }, + "type'": "Resource" + }, + { + "resourceChange": { + "action": "Modify", + "changeSetId": null, + "details": [ + { + "causingEntity": null, + "changeSource": null, + "evaluation": "Static", + "target": { + "attribute": "Tags", + "name": null, + "requiresRecreation": "Never" + } + } + ], + "logicalResourceId": "ECSCluster", + "moduleInfo": null, + "physicalResourceId": "prod-faktory", + "replacement": "False", + "resourceType": "AWS::ECS::Cluster", + "scope": [ + "Tags" + ] + }, + "type'": "Resource" + }, + { + "resourceChange": { + "action": "Modify", + "changeSetId": null, + "details": [ + { + "causingEntity": null, + "changeSource": null, + "evaluation": "Static", + "target": { + "attribute": "Tags", + "name": null, + "requiresRecreation": "Never" + } + } + ], + "logicalResourceId": "EFSFileSystem", + "moduleInfo": null, + "physicalResourceId": "fs-9529dc77", + "replacement": "False", + "resourceType": "AWS::EFS::FileSystem", + "scope": [ + "Tags" + ] + }, + "type'": "Resource" + }, + { + "resourceChange": { + "action": "Modify", + "changeSetId": null, + "details": [ + { + "causingEntity": null, + "changeSource": null, + "evaluation": "Static", + "target": { + "attribute": "Tags", + "name": null, + "requiresRecreation": "Never" + } + }, + { + "causingEntity": "EFSSecurityGroup", + "changeSource": "ResourceReference", + "evaluation": "Static", + "target": { + "attribute": "Properties", + "name": "SecurityGroups", + "requiresRecreation": "Never" + } + }, + { + "causingEntity": null, + "changeSource": "DirectModification", + "evaluation": "Static", + "target": { + "attribute": "Properties", + "name": "SubnetId", + "requiresRecreation": "Always" + } + } + ], + "logicalResourceId": "EFSMountTarget", + "moduleInfo": null, + "physicalResourceId": "fsmt-e4d41504", + "replacement": "True", + "resourceType": "AWS::EFS::MountTarget", + "scope": [ + "Properties", + "Tags" + ] + }, + "type'": "Resource" + }, + { + "resourceChange": { + "action": "Modify", + "changeSetId": null, + "details": [ + { + "causingEntity": "ClusterSecurityGroup", + "changeSource": "ResourceReference", + "evaluation": "Static", + "target": { + "attribute": "Properties", + "name": "SecurityGroupIngress", + "requiresRecreation": "Never" + } + }, + { + "causingEntity": null, + "changeSource": "DirectModification", + "evaluation": "Dynamic", + "target": { + "attribute": "Properties", + "name": "SecurityGroupIngress", + "requiresRecreation": "Never" + } + }, + { + "causingEntity": null, + "changeSource": null, + "evaluation": "Static", + "target": { + "attribute": "Tags", + "name": null, + "requiresRecreation": "Never" + } + }, + { + "causingEntity": null, + "changeSource": "DirectModification", + "evaluation": "Static", + "target": { + "attribute": "Properties", + "name": "VpcId", + "requiresRecreation": "Always" + } + } + ], + "logicalResourceId": "EFSSecurityGroup", + "moduleInfo": null, + "physicalResourceId": "sg-07bd7bd588bafb48b", + "replacement": "True", + "resourceType": "AWS::EC2::SecurityGroup", + "scope": [ + "Properties", + "Tags" + ] + }, + "type'": "Resource" + }, + { + "resourceChange": { + "action": "Modify", + "changeSetId": null, + "details": [ + { + "causingEntity": null, + "changeSource": null, + "evaluation": "Static", + "target": { + "attribute": "Tags", + "name": null, + "requiresRecreation": "Never" + } + } + ], + "logicalResourceId": "InstanceProfile", + "moduleInfo": null, + "physicalResourceId": "prod-faktory-ecs-cluster-InstanceProfile-1W0KE43Y2TWDU", + "replacement": "False", + "resourceType": "AWS::IAM::InstanceProfile", + "scope": [ + "Tags" + ] + }, + "type'": "Resource" + }, + { + "resourceChange": { + "action": "Modify", + "changeSetId": null, + "details": [ + { + "causingEntity": null, + "changeSource": "DirectModification", + "evaluation": "Static", + "target": { + "attribute": "Properties", + "name": "Policies", + "requiresRecreation": "Never" + } + }, + { + "causingEntity": null, + "changeSource": null, + "evaluation": "Static", + "target": { + "attribute": "Tags", + "name": null, + "requiresRecreation": "Never" + } + } + ], + "logicalResourceId": "InstanceRole", + "moduleInfo": null, + "physicalResourceId": "prod-faktory-ecs-cluster-InstanceRole-1EEWHD4VKB56Y", + "replacement": "False", + "resourceType": "AWS::IAM::Role", + "scope": [ + "Properties", + "Tags" + ] + }, + "type'": "Resource" + }, + { + "resourceChange": { + "action": "Modify", + "changeSetId": null, + "details": [ + { + "causingEntity": null, + "changeSource": null, + "evaluation": "Static", + "target": { + "attribute": "Tags", + "name": null, + "requiresRecreation": "Never" + } + }, + { + "causingEntity": null, + "changeSource": "DirectModification", + "evaluation": "Static", + "target": { + "attribute": "Properties", + "name": "ImageId", + "requiresRecreation": "Always" + } + }, + { + "causingEntity": null, + "changeSource": "DirectModification", + "evaluation": "Static", + "target": { + "attribute": "Properties", + "name": "UserData", + "requiresRecreation": "Always" + } + }, + { + "causingEntity": "ClusterSecurityGroup", + "changeSource": "ResourceReference", + "evaluation": "Static", + "target": { + "attribute": "Properties", + "name": "SecurityGroups", + "requiresRecreation": "Always" + } + } + ], + "logicalResourceId": "LaunchConfiguration", + "moduleInfo": null, + "physicalResourceId": "prod-faktory-ecs-cluster-LaunchConfiguration-Mko0Jp8XTuKr", + "replacement": "True", + "resourceType": "AWS::AutoScaling::LaunchConfiguration", + "scope": [ + "Properties", + "Tags" + ] + }, + "type'": "Resource" + }, + { + "resourceChange": { + "action": "Modify", + "changeSetId": null, + "details": [ + { + "causingEntity": "NLB.CanonicalHostedZoneID", + "changeSource": "ResourceAttribute", + "evaluation": "Dynamic", + "target": { + "attribute": "Properties", + "name": "AliasTarget", + "requiresRecreation": "Never" + } + }, + { + "causingEntity": null, + "changeSource": "DirectModification", + "evaluation": "Dynamic", + "target": { + "attribute": "Properties", + "name": "AliasTarget", + "requiresRecreation": "Never" + } + }, + { + "causingEntity": "NLB.DNSName", + "changeSource": "ResourceAttribute", + "evaluation": "Dynamic", + "target": { + "attribute": "Properties", + "name": "AliasTarget", + "requiresRecreation": "Never" + } + } + ], + "logicalResourceId": "NLBDNSAliasRecord", + "moduleInfo": null, + "physicalResourceId": "faktory-internal.freckle.com", + "replacement": "False", + "resourceType": "AWS::Route53::RecordSet", + "scope": [ + "Properties" + ] + }, + "type'": "Resource" + }, + { + "resourceChange": { + "action": "Modify", + "changeSetId": null, + "details": [ + { + "causingEntity": "NLBTargetGroup", + "changeSource": "ResourceReference", + "evaluation": "Static", + "target": { + "attribute": "Properties", + "name": "DefaultActions", + "requiresRecreation": "Never" + } + }, + { + "causingEntity": null, + "changeSource": null, + "evaluation": "Static", + "target": { + "attribute": "Tags", + "name": null, + "requiresRecreation": "Never" + } + } + ], + "logicalResourceId": "NLBListener", + "moduleInfo": null, + "physicalResourceId": "arn:aws:elasticloadbalancing:us-east-1:999999999999:listener/net/prod-fa-NLB-HHEBROLH5J6T/6b647d08dcc2d594/80372abc7d907982", + "replacement": "False", + "resourceType": "AWS::ElasticLoadBalancingV2::Listener", + "scope": [ + "Properties", + "Tags" + ] + }, + "type'": "Resource" + }, + { + "resourceChange": { + "action": "Modify", + "changeSetId": null, + "details": [ + { + "causingEntity": null, + "changeSource": null, + "evaluation": "Static", + "target": { + "attribute": "Tags", + "name": null, + "requiresRecreation": "Never" + } + }, + { + "causingEntity": null, + "changeSource": "DirectModification", + "evaluation": "Static", + "target": { + "attribute": "Properties", + "name": "VpcId", + "requiresRecreation": "Always" + } + } + ], + "logicalResourceId": "NLBTargetGroup", + "moduleInfo": null, + "physicalResourceId": "arn:aws:elasticloadbalancing:us-east-1:999999999999:targetgroup/prod-NLBTa-RNPHZHDRPVIZ/73c88e6d9e0e5b01", + "replacement": "True", + "resourceType": "AWS::ElasticLoadBalancingV2::TargetGroup", + "scope": [ + "Properties", + "Tags" + ] + }, + "type'": "Resource" + }, + { + "resourceChange": { + "action": "Modify", + "changeSetId": null, + "details": [ + { + "causingEntity": null, + "changeSource": "DirectModification", + "evaluation": "Static", + "target": { + "attribute": "Properties", + "name": "Subnets", + "requiresRecreation": "Never" + } + }, + { + "causingEntity": null, + "changeSource": null, + "evaluation": "Static", + "target": { + "attribute": "Tags", + "name": null, + "requiresRecreation": "Never" + } + } + ], + "logicalResourceId": "NLB", + "moduleInfo": null, + "physicalResourceId": "arn:aws:elasticloadbalancing:us-east-1:999999999999:loadbalancer/net/prod-fa-NLB-HHEBROLH5J6T/6b647d08dcc2d594", + "replacement": "False", + "resourceType": "AWS::ElasticLoadBalancingV2::LoadBalancer", + "scope": [ + "Properties", + "Tags" + ] + }, + "type'": "Resource" + }, + { + "resourceChange": { + "action": "Modify", + "changeSetId": null, + "details": [ + { + "causingEntity": null, + "changeSource": null, + "evaluation": "Static", + "target": { + "attribute": "Tags", + "name": null, + "requiresRecreation": "Never" + } + } + ], + "logicalResourceId": "SNSLambdaRole", + "moduleInfo": null, + "physicalResourceId": "prod-faktory-ecs-cluster-SNSLambdaRole-YSXPOSGWHAYA", + "replacement": "False", + "resourceType": "AWS::IAM::Role", + "scope": [ + "Tags" + ] + }, + "type'": "Resource" + }, + { + "resourceChange": { + "action": "Modify", + "changeSetId": null, + "details": [ + { + "causingEntity": null, + "changeSource": null, + "evaluation": "Static", + "target": { + "attribute": "Tags", + "name": null, + "requiresRecreation": "Never" + } + }, + { + "causingEntity": "AutoScalingGroup", + "changeSource": "ResourceReference", + "evaluation": "Dynamic", + "target": { + "attribute": "Properties", + "name": "AutoScalingGroupName", + "requiresRecreation": "Always" + } + } + ], + "logicalResourceId": "ScaleDownScheduledAction", + "moduleInfo": null, + "physicalResourceId": "prod-Scale-1KE1OLZRQSSXA", + "replacement": "Conditional", + "resourceType": "AWS::AutoScaling::ScheduledAction", + "scope": [ + "Properties", + "Tags" + ] + }, + "type'": "Resource" + }, + { + "resourceChange": { + "action": "Modify", + "changeSetId": null, + "details": [ + { + "causingEntity": null, + "changeSource": null, + "evaluation": "Static", + "target": { + "attribute": "Tags", + "name": null, + "requiresRecreation": "Never" + } + }, + { + "causingEntity": "AutoScalingGroup", + "changeSource": "ResourceReference", + "evaluation": "Dynamic", + "target": { + "attribute": "Properties", + "name": "AutoScalingGroupName", + "requiresRecreation": "Always" + } + } + ], + "logicalResourceId": "ScaleUpScheduledAction", + "moduleInfo": null, + "physicalResourceId": "prod-Scale-13PIRUZGZW86O", + "replacement": "Conditional", + "resourceType": "AWS::AutoScaling::ScheduledAction", + "scope": [ + "Properties", + "Tags" + ] + }, + "type'": "Resource" + } + ], + "creationTime": "2023-07-31T20:11:25Z", + "description": null, + "executionStatus": "AVAILABLE", + "httpStatus": 200, + "includeNestedStacks": false, + "nextToken": null, + "notificationARNs": null, + "parameters": [ + { + "parameterKey": "IngressFrom", + "parameterValue": "7419", + "resolvedValue": null, + "usePreviousValue": null + }, + { + "parameterKey": "IngressTo", + "parameterValue": "7420", + "resolvedValue": null, + "usePreviousValue": null + }, + { + "parameterKey": "ScaleUpRecurrence", + "parameterValue": "0 1 * * 2", + "resolvedValue": null, + "usePreviousValue": null + }, + { + "parameterKey": "NLBSubDomain", + "parameterValue": "faktory-internal", + "resolvedValue": null, + "usePreviousValue": null + }, + { + "parameterKey": "ClusterRole", + "parameterValue": null, + "resolvedValue": null, + "usePreviousValue": null + }, + { + "parameterKey": "Name", + "parameterValue": "faktory", + "resolvedValue": null, + "usePreviousValue": null + }, + { + "parameterKey": "NLBDomain", + "parameterValue": "freckle.com", + "resolvedValue": null, + "usePreviousValue": null + }, + { + "parameterKey": "ProvisionedThroughputInMibps", + "parameterValue": "3", + "resolvedValue": null, + "usePreviousValue": null + }, + { + "parameterKey": "Environment", + "parameterValue": "prod", + "resolvedValue": null, + "usePreviousValue": null + }, + { + "parameterKey": "NLBPort", + "parameterValue": "7419", + "resolvedValue": null, + "usePreviousValue": null + }, + { + "parameterKey": "InstanceType", + "parameterValue": "t3.medium", + "resolvedValue": null, + "usePreviousValue": null + }, + { + "parameterKey": "ECSAMI", + "parameterValue": "/aws/service/ecs/optimized-ami/amazon-linux-2/recommended/image_id", + "resolvedValue": "ami-05aca8932be1b68c3", + "usePreviousValue": null + }, + { + "parameterKey": "ScaleDownRecurrence", + "parameterValue": "10 1 * * 2", + "resolvedValue": null, + "usePreviousValue": null + } + ], + "parentChangeSetId": null, + "rollbackConfiguration": null, + "rootChangeSetId": null, + "stackId": "arn:aws:cloudformation:us-east-1:999999999999:stack/prod-faktory-ecs-cluster/f09dbb60-b244-11e9-a24c-120371d9064c", + "stackName": "prod-faktory-ecs-cluster", + "status": "CREATE_COMPLETE", + "statusReason": null, + "tags": [ + { + "key": "Owner", + "value": "Platform" + }, + { + "key": "DeployedBy", + "value": "github:freckle/infa" + }, + { + "key": "service", + "value": "faktory" + }, + { + "key": "env", + "value": "prod" + } + ] +} diff --git a/test/files/change-sets/prod-faktory.md b/test/files/change-sets/prod-faktory.md new file mode 100644 index 0000000..b5f37da --- /dev/null +++ b/test/files/change-sets/prod-faktory.md @@ -0,0 +1,27 @@ +:warning: This PR generates **23** changes for `some-stack`. + +| Action | Logical Id | Physical Id | Type | Replacement | Scope | Details | +| --- | --- | --- | --- | --- | --- | --- | +| Add | ASGDepends | | AWS::CloudFormation::WaitConditionHandle | | | | +| Modify | ASGLambdaExecutionRole | prod-faktory-ecs-cluster-ASGLambdaExecutionRole-U9NPRW10WHGZ | AWS::IAM::Role | False | Tags |
    | +| Modify | ASGLifecycleLambdaFunction | prod-faktory-ecs-cluster-ASGLifecycleLambdaFunctio-C1RAZ4U57W7R | AWS::Lambda::Function | False | Properties, Tags |
    • DirectModification in Properties (Role), recreation Never
    • ResourceAttribute in Properties (Role), recreation Never, caused by ASGLambdaExecutionRole.Arn
    | +| Modify | ASGLambdaInvokePermission | prod-faktory-ecs-cluster-ASGLambdaInvokePermission-1475JOG38K6P | AWS::Lambda::Permission | Conditional | Properties, Tags |
    • DirectModification in Properties (FunctionName), recreation Always
    • ResourceAttribute in Properties (FunctionName), recreation Always, caused by ASGLifecycleLambdaFunction.Arn
    | +| Modify | ASGLambdaSNSSubscription | arn:aws:sns:us-east-1:999999999999:prod-faktory-ecs-cluster-ASGSNSTopic-1GZCULO2RMPPZ:4fec6f06-f72b-488b-9d97-c7a4621be7ca | AWS::SNS::Subscription | Conditional | Properties |
    • ResourceAttribute in Properties (Endpoint), recreation Always, caused by ASGLifecycleLambdaFunction.Arn
    • DirectModification in Properties (Endpoint), recreation Always
    | +| Modify | ASGSNSTopic | arn:aws:sns:us-east-1:999999999999:prod-faktory-ecs-cluster-ASGSNSTopic-1GZCULO2RMPPZ | AWS::SNS::Topic | False | Properties, Tags |
    • ResourceAttribute in Properties (Subscription), recreation Never, caused by ASGLifecycleLambdaFunction.Arn
    • DirectModification in Properties (Subscription), recreation Never
    | +| Modify | SNSLambdaRole | prod-faktory-ecs-cluster-SNSLambdaRole-YSXPOSGWHAYA | AWS::IAM::Role | False | Tags |
      | +| Modify | ClusterSecurityGroup | sg-04a524560191c2e42 | AWS::EC2::SecurityGroup | True | Properties, Tags |
      • DirectModification in Properties (GroupDescription), recreation Always
      • DirectModification in Properties (SecurityGroupIngress), recreation Never
      • ParameterReference in Properties (SecurityGroupIngress), recreation Never, caused by IngressTo
      • DirectModification in Properties (VpcId), recreation Always
      • ParameterReference in Properties (GroupDescription), recreation Always, caused by Name
      • ParameterReference in Properties (SecurityGroupIngress), recreation Never, caused by IngressFrom
      | +| Modify | LaunchConfiguration | prod-faktory-ecs-cluster-LaunchConfiguration-Mko0Jp8XTuKr | AWS::AutoScaling::LaunchConfiguration | True | Properties, Tags |
      • DirectModification in Properties (ImageId), recreation Always
      • DirectModification in Properties (UserData), recreation Always
      • ResourceReference in Properties (SecurityGroups), recreation Always, caused by ClusterSecurityGroup
      | +| Modify | NLBTargetGroup | arn:aws:elasticloadbalancing:us-east-1:999999999999:targetgroup/prod-NLBTa-RNPHZHDRPVIZ/73c88e6d9e0e5b01 | AWS::ElasticLoadBalancingV2::TargetGroup | True | Properties, Tags |
      • DirectModification in Properties (VpcId), recreation Always
      | +| Modify | AutoScalingGroup | prod-faktory-ecs-cluster-AutoScalingGroup-13NOD2PK314EV | AWS::AutoScaling::AutoScalingGroup | Conditional | Properties, Tags |
      • ResourceReference in Properties (LaunchConfigurationName), recreation Conditionally, caused by LaunchConfiguration
      • ResourceReference in Properties (TargetGroupARNs), recreation Never, caused by NLBTargetGroup
      • DirectModification in Properties (VPCZoneIdentifier), recreation Conditionally
      • ParameterReference in Tags, recreation Never, caused by Environment
      • DirectModification in Tags, recreation Never
      | +| Modify | ASGTerminateHook | prod-faktory-ecs-cluster-ASGTerminateHook-18CDA25XDPELR | AWS::AutoScaling::LifecycleHook | Conditional | Properties, Tags |
      • ResourceAttribute in Properties (RoleARN), recreation Never, caused by SNSLambdaRole.Arn
      • DirectModification in Properties (RoleARN), recreation Never
      • ResourceReference in Properties (AutoScalingGroupName), recreation Always, caused by AutoScalingGroup
      | +| Modify | ECSCluster | prod-faktory | AWS::ECS::Cluster | False | Tags |
        | +| Modify | EFSFileSystem | fs-9529dc77 | AWS::EFS::FileSystem | False | Tags |
          | +| Modify | EFSSecurityGroup | sg-07bd7bd588bafb48b | AWS::EC2::SecurityGroup | True | Properties, Tags |
          • ResourceReference in Properties (SecurityGroupIngress), recreation Never, caused by ClusterSecurityGroup
          • DirectModification in Properties (SecurityGroupIngress), recreation Never
          • DirectModification in Properties (VpcId), recreation Always
          | +| Modify | EFSMountTarget | fsmt-e4d41504 | AWS::EFS::MountTarget | True | Properties, Tags |
          • ResourceReference in Properties (SecurityGroups), recreation Never, caused by EFSSecurityGroup
          • DirectModification in Properties (SubnetId), recreation Always
          | +| Modify | InstanceProfile | prod-faktory-ecs-cluster-InstanceProfile-1W0KE43Y2TWDU | AWS::IAM::InstanceProfile | False | Tags |
            | +| Modify | InstanceRole | prod-faktory-ecs-cluster-InstanceRole-1EEWHD4VKB56Y | AWS::IAM::Role | False | Properties, Tags |
            • DirectModification in Properties (Policies), recreation Never
            | +| Modify | NLB | arn:aws:elasticloadbalancing:us-east-1:999999999999:loadbalancer/net/prod-fa-NLB-HHEBROLH5J6T/6b647d08dcc2d594 | AWS::ElasticLoadBalancingV2::LoadBalancer | False | Properties, Tags |
            • DirectModification in Properties (Subnets), recreation Never
            | +| Modify | NLBDNSAliasRecord | faktory-internal.freckle.com | AWS::Route53::RecordSet | False | Properties |
            • ResourceAttribute in Properties (AliasTarget), recreation Never, caused by NLB.CanonicalHostedZoneID
            • DirectModification in Properties (AliasTarget), recreation Never
            • ResourceAttribute in Properties (AliasTarget), recreation Never, caused by NLB.DNSName
            | +| Modify | NLBListener | arn:aws:elasticloadbalancing:us-east-1:999999999999:listener/net/prod-fa-NLB-HHEBROLH5J6T/6b647d08dcc2d594/80372abc7d907982 | AWS::ElasticLoadBalancingV2::Listener | False | Properties, Tags |
            • ResourceReference in Properties (DefaultActions), recreation Never, caused by NLBTargetGroup
            | +| Modify | ScaleDownScheduledAction | prod-Scale-1KE1OLZRQSSXA | AWS::AutoScaling::ScheduledAction | Conditional | Properties, Tags |
            • ResourceReference in Properties (AutoScalingGroupName), recreation Always, caused by AutoScalingGroup
            | +| Modify | ScaleUpScheduledAction | prod-Scale-13PIRUZGZW86O | AWS::AutoScaling::ScheduledAction | Conditional | Properties, Tags |
            • ResourceReference in Properties (AutoScalingGroupName), recreation Always, caused by AutoScalingGroup
            | diff --git a/test/files/change-sets/prod-faktory.txt b/test/files/change-sets/prod-faktory.txt new file mode 100644 index 0000000..7b09992 --- /dev/null +++ b/test/files/change-sets/prod-faktory.txt @@ -0,0 +1,130 @@ + +Changes for some-stack: + Add ASGDepends (AWS::CloudFormation::WaitConditionHandle) + Modify ASGLambdaExecutionRole (AWS::IAM::Role) prod-faktory-ecs-cluster-ASGLambdaExecutionRole-U9NPRW10WHGZ + Replacement: False + Scope: Tags + Details: + Modify ASGLifecycleLambdaFunction (AWS::Lambda::Function) prod-faktory-ecs-cluster-ASGLifecycleLambdaFunctio-C1RAZ4U57W7R + Replacement: False + Scope: Properties, Tags + Details: + * DirectModification in Properties (Role), recreation Never + * ResourceAttribute in Properties (Role), recreation Never, caused by ASGLambdaExecutionRole.Arn + Modify ASGLambdaInvokePermission (AWS::Lambda::Permission) prod-faktory-ecs-cluster-ASGLambdaInvokePermission-1475JOG38K6P + Replacement: Conditional + Scope: Properties, Tags + Details: + * DirectModification in Properties (FunctionName), recreation Always + * ResourceAttribute in Properties (FunctionName), recreation Always, caused by ASGLifecycleLambdaFunction.Arn + Modify ASGLambdaSNSSubscription (AWS::SNS::Subscription) arn:aws:sns:us-east-1:999999999999:prod-faktory-ecs-cluster-ASGSNSTopic-1GZCULO2RMPPZ:4fec6f06-f72b-488b-9d97-c7a4621be7ca + Replacement: Conditional + Scope: Properties + Details: + * ResourceAttribute in Properties (Endpoint), recreation Always, caused by ASGLifecycleLambdaFunction.Arn + * DirectModification in Properties (Endpoint), recreation Always + Modify ASGSNSTopic (AWS::SNS::Topic) arn:aws:sns:us-east-1:999999999999:prod-faktory-ecs-cluster-ASGSNSTopic-1GZCULO2RMPPZ + Replacement: False + Scope: Properties, Tags + Details: + * ResourceAttribute in Properties (Subscription), recreation Never, caused by ASGLifecycleLambdaFunction.Arn + * DirectModification in Properties (Subscription), recreation Never + Modify SNSLambdaRole (AWS::IAM::Role) prod-faktory-ecs-cluster-SNSLambdaRole-YSXPOSGWHAYA + Replacement: False + Scope: Tags + Details: + Modify ClusterSecurityGroup (AWS::EC2::SecurityGroup) sg-04a524560191c2e42 + Replacement: True + Scope: Properties, Tags + Details: + * DirectModification in Properties (GroupDescription), recreation Always + * DirectModification in Properties (SecurityGroupIngress), recreation Never + * ParameterReference in Properties (SecurityGroupIngress), recreation Never, caused by IngressTo + * DirectModification in Properties (VpcId), recreation Always + * ParameterReference in Properties (GroupDescription), recreation Always, caused by Name + * ParameterReference in Properties (SecurityGroupIngress), recreation Never, caused by IngressFrom + Modify LaunchConfiguration (AWS::AutoScaling::LaunchConfiguration) prod-faktory-ecs-cluster-LaunchConfiguration-Mko0Jp8XTuKr + Replacement: True + Scope: Properties, Tags + Details: + * DirectModification in Properties (ImageId), recreation Always + * DirectModification in Properties (UserData), recreation Always + * ResourceReference in Properties (SecurityGroups), recreation Always, caused by ClusterSecurityGroup + Modify NLBTargetGroup (AWS::ElasticLoadBalancingV2::TargetGroup) arn:aws:elasticloadbalancing:us-east-1:999999999999:targetgroup/prod-NLBTa-RNPHZHDRPVIZ/73c88e6d9e0e5b01 + Replacement: True + Scope: Properties, Tags + Details: + * DirectModification in Properties (VpcId), recreation Always + Modify AutoScalingGroup (AWS::AutoScaling::AutoScalingGroup) prod-faktory-ecs-cluster-AutoScalingGroup-13NOD2PK314EV + Replacement: Conditional + Scope: Properties, Tags + Details: + * ResourceReference in Properties (LaunchConfigurationName), recreation Conditionally, caused by LaunchConfiguration + * ResourceReference in Properties (TargetGroupARNs), recreation Never, caused by NLBTargetGroup + * DirectModification in Properties (VPCZoneIdentifier), recreation Conditionally + * ParameterReference in Tags, recreation Never, caused by Environment + * DirectModification in Tags, recreation Never + Modify ASGTerminateHook (AWS::AutoScaling::LifecycleHook) prod-faktory-ecs-cluster-ASGTerminateHook-18CDA25XDPELR + Replacement: Conditional + Scope: Properties, Tags + Details: + * ResourceAttribute in Properties (RoleARN), recreation Never, caused by SNSLambdaRole.Arn + * DirectModification in Properties (RoleARN), recreation Never + * ResourceReference in Properties (AutoScalingGroupName), recreation Always, caused by AutoScalingGroup + Modify ECSCluster (AWS::ECS::Cluster) prod-faktory + Replacement: False + Scope: Tags + Details: + Modify EFSFileSystem (AWS::EFS::FileSystem) fs-9529dc77 + Replacement: False + Scope: Tags + Details: + Modify EFSSecurityGroup (AWS::EC2::SecurityGroup) sg-07bd7bd588bafb48b + Replacement: True + Scope: Properties, Tags + Details: + * ResourceReference in Properties (SecurityGroupIngress), recreation Never, caused by ClusterSecurityGroup + * DirectModification in Properties (SecurityGroupIngress), recreation Never + * DirectModification in Properties (VpcId), recreation Always + Modify EFSMountTarget (AWS::EFS::MountTarget) fsmt-e4d41504 + Replacement: True + Scope: Properties, Tags + Details: + * ResourceReference in Properties (SecurityGroups), recreation Never, caused by EFSSecurityGroup + * DirectModification in Properties (SubnetId), recreation Always + Modify InstanceProfile (AWS::IAM::InstanceProfile) prod-faktory-ecs-cluster-InstanceProfile-1W0KE43Y2TWDU + Replacement: False + Scope: Tags + Details: + Modify InstanceRole (AWS::IAM::Role) prod-faktory-ecs-cluster-InstanceRole-1EEWHD4VKB56Y + Replacement: False + Scope: Properties, Tags + Details: + * DirectModification in Properties (Policies), recreation Never + Modify NLB (AWS::ElasticLoadBalancingV2::LoadBalancer) arn:aws:elasticloadbalancing:us-east-1:999999999999:loadbalancer/net/prod-fa-NLB-HHEBROLH5J6T/6b647d08dcc2d594 + Replacement: False + Scope: Properties, Tags + Details: + * DirectModification in Properties (Subnets), recreation Never + Modify NLBDNSAliasRecord (AWS::Route53::RecordSet) faktory-internal.freckle.com + Replacement: False + Scope: Properties + Details: + * ResourceAttribute in Properties (AliasTarget), recreation Never, caused by NLB.CanonicalHostedZoneID + * DirectModification in Properties (AliasTarget), recreation Never + * ResourceAttribute in Properties (AliasTarget), recreation Never, caused by NLB.DNSName + Modify NLBListener (AWS::ElasticLoadBalancingV2::Listener) arn:aws:elasticloadbalancing:us-east-1:999999999999:listener/net/prod-fa-NLB-HHEBROLH5J6T/6b647d08dcc2d594/80372abc7d907982 + Replacement: False + Scope: Properties, Tags + Details: + * ResourceReference in Properties (DefaultActions), recreation Never, caused by NLBTargetGroup + Modify ScaleDownScheduledAction (AWS::AutoScaling::ScheduledAction) prod-Scale-1KE1OLZRQSSXA + Replacement: Conditional + Scope: Properties, Tags + Details: + * ResourceReference in Properties (AutoScalingGroupName), recreation Always, caused by AutoScalingGroup + Modify ScaleUpScheduledAction (AWS::AutoScaling::ScheduledAction) prod-Scale-13PIRUZGZW86O + Replacement: Conditional + Scope: Properties, Tags + Details: + * ResourceReference in Properties (AutoScalingGroupName), recreation Always, caused by AutoScalingGroup From b686bdd44b2c1af1d7893c44e5385a4bf30bc998 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Thu, 31 Aug 2023 09:36:39 -0400 Subject: [PATCH 083/187] Disable cabal-fmt and exclude test files --- .restyled.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.restyled.yaml b/.restyled.yaml index f3c387a..58e337b 100644 --- a/.restyled.yaml +++ b/.restyled.yaml @@ -1,5 +1,7 @@ restylers_version: dev restylers: + - cabal-fmt: + enabled: false - fourmolu - stylish-haskell: enabled: false @@ -11,3 +13,6 @@ restylers: - "!**/*.t" # cram tests have whitespace in assertions - "!README.md" # help code blocks have trailing whitespace - "*" + +also_exclude: + - "test/files/**/*" From 700e15dbe325dafcc9aa5a2f11ebd724da3e9986 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 30 Aug 2023 08:46:48 -0400 Subject: [PATCH 084/187] Fix auto-expansion of filter option Using (e.g.) `--filter progress` is meant to automatically find all stacks related to this by turning that into a series of globs. Part of that was turning it into `progress/**`, which is meant to find all sub-directories. This was incorrect and needed to be `progress/**/*` for that to work reliably. This small commit fixes that. --- src/Stackctl/FilterOption.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Stackctl/FilterOption.hs b/src/Stackctl/FilterOption.hs index 985dfbe..0310e50 100644 --- a/src/Stackctl/FilterOption.hs +++ b/src/Stackctl/FilterOption.hs @@ -77,7 +77,7 @@ expandPatterns t = map compile $ s : expanded suffixed | "*" `T.isSuffixOf` t || hasExtension s = [] - | otherwise = (s "*") : map (s <.>) extensions + | otherwise = (s "**" "*") : map (s <.>) extensions extensions = ["json", "yaml"] From 1323bdf6199de16173187130d5c58840601e729c Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 29 Aug 2023 17:29:44 -0400 Subject: [PATCH 085/187] Simplify awsSimple Amazonka-2.0 adds a `Typeable` constraint to its `send`-et-al functions, presumably because it's using `typeRep` to render the type of response in its own logging. This means we too can use `typeRep` for our own error message in `awsSimple` and thus not need to pass in an argument for that. Passing this argument was always an annoying bit of boilerplate and subject to inconsistencies (sometimes we'd prefix with the service, sometimes not, for example). This is more convenient and consistent. The only downsides are churn for callers and requiring a major version bump, which is acceptable. --- src/Stackctl/AWS/CloudFormation.hs | 12 ++++++------ src/Stackctl/AWS/Core.hs | 19 ++++++++++++------- src/Stackctl/AWS/EC2.hs | 2 +- src/Stackctl/AWS/STS.hs | 2 +- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/Stackctl/AWS/CloudFormation.hs b/src/Stackctl/AWS/CloudFormation.hs index b764ef6..61282ff 100644 --- a/src/Stackctl/AWS/CloudFormation.hs +++ b/src/Stackctl/AWS/CloudFormation.hs @@ -176,7 +176,7 @@ awsCloudFormationDescribeStack awsCloudFormationDescribeStack stackName = do let req = newDescribeStacks & describeStacks_stackName ?~ unStackName stackName - awsSimple "DescribeStack" req $ \resp -> do + awsSimple req $ \resp -> do stacks <- resp ^. describeStacksResponse_stacks listToMaybe stacks @@ -252,7 +252,7 @@ awsCloudFormationGetMostRecentStackEventId stackName = do [] -> Nothing (e : _) -> Just $ e ^. stackEvent_eventId - awsSimple "DescribeStackEvents" req + awsSimple req $ pure . getFirstEventId . fromMaybe [] @@ -268,7 +268,7 @@ awsCloudFormationDeleteStack stackName = do describeReq = newDescribeStacks & describeStacks_stackName ?~ unStackName stackName - awsSimple "DeleteStack" deleteReq $ const $ pure () + awsSimple deleteReq $ const $ pure () logDebug "Awaiting DeleteStack" stackDeleteResult <$> awsAwait newStackDeleteComplete describeReq @@ -298,7 +298,7 @@ awsCloudFormationGetTemplate stackName = do decodeTemplateBody body = fromMaybe (toJSON body) $ decodeStrict $ encodeUtf8 body - awsSimple "GetTemplate" req $ \resp -> do + awsSimple req $ \resp -> do body <- resp ^. getTemplateResponse_templateBody pure $ decodeTemplateBody body @@ -409,7 +409,7 @@ awsCloudFormationCreateChangeSet stackName mStackDescription stackTemplate param logInfo $ "Creating changeset..." :# ["name" .= name, "type" .= changeSetType] - csId <- awsSimple "CreateChangeSet" req (^. createChangeSetResponse_id) + csId <- awsSimple req (^. createChangeSetResponse_id) logDebug "Awaiting CREATE_COMPLETE" void $ awsAwait newChangeSetCreateComplete $ newDescribeChangeSet csId @@ -424,7 +424,7 @@ awsCloudFormationDescribeChangeSet -> m ChangeSet awsCloudFormationDescribeChangeSet changeSetId = do let req = newDescribeChangeSet $ unChangeSetId changeSetId - awsSimple "DescribeChangeSet" req changeSetFromResponse + awsSimple req changeSetFromResponse sortChanges :: [Change] -> [Change] sortChanges = sortByDependencies changeName changeCausedBy diff --git a/src/Stackctl/AWS/Core.hs b/src/Stackctl/AWS/Core.hs index 55424c9..41587df 100644 --- a/src/Stackctl/AWS/Core.hs +++ b/src/Stackctl/AWS/Core.hs @@ -39,6 +39,7 @@ import Amazonka.STS.AssumeRole import Conduit (ConduitM) import Control.Monad.Logger (defaultLoc, toLogStr) import Control.Monad.Trans.Resource (MonadResource) +import Data.Typeable (typeRep) import Stackctl.AWS.Orphans () import UnliftIO.Exception.Lens (handling) @@ -84,22 +85,26 @@ awsWithAuth f = do withAuth auth f awsSimple - :: ( MonadResource m + :: forall a env m b + . ( HasCallStack + , MonadResource m , MonadReader env m , HasAwsEnv env , AWSRequest a , Typeable a , Typeable (AWSResponse a) ) - => Text - -> a + => a -> (AWSResponse a -> Maybe b) -> m b -awsSimple name req post = do +awsSimple req post = do resp <- awsSend req + + let + name = show $ typeRep $ Proxy @a + err = name <> " successful, but processing the response failed" + maybe (throwString err) pure $ post resp - where - err = unpack name <> " successful, but processing the response failed" awsSend :: ( MonadResource m @@ -158,7 +163,7 @@ awsAssumeRole awsAssumeRole role sessionName f = do let req = newAssumeRole role sessionName - assumeEnv <- awsSimple "sts:AssumeRole" req $ \resp -> do + assumeEnv <- awsSimple req $ \resp -> do let creds = resp ^. assumeRoleResponse_credentials token <- creds ^. authEnv_sessionToken diff --git a/src/Stackctl/AWS/EC2.hs b/src/Stackctl/AWS/EC2.hs index 92528f5..0a732f1 100644 --- a/src/Stackctl/AWS/EC2.hs +++ b/src/Stackctl/AWS/EC2.hs @@ -12,7 +12,7 @@ awsEc2DescribeFirstAvailabilityZoneRegionName :: (MonadResource m, MonadReader env m, HasAwsEnv env) => m Region awsEc2DescribeFirstAvailabilityZoneRegionName = do let req = newDescribeAvailabilityZones - awsSimple "DescribeAvailabilityZones" req $ \resp -> do + awsSimple req $ \resp -> do azs <- resp ^. describeAvailabilityZonesResponse_availabilityZones az <- listToMaybe azs rn <- regionName az diff --git a/src/Stackctl/AWS/STS.hs b/src/Stackctl/AWS/STS.hs index 316da27..41b7538 100644 --- a/src/Stackctl/AWS/STS.hs +++ b/src/Stackctl/AWS/STS.hs @@ -10,5 +10,5 @@ import Stackctl.AWS.Core awsGetCallerIdentityAccount :: (MonadResource m, MonadReader env m, HasAwsEnv env) => m AccountId awsGetCallerIdentityAccount = do - awsSimple "GetCallerIdentity" newGetCallerIdentity $ \resp -> do + awsSimple newGetCallerIdentity $ \resp -> do AccountId <$> resp ^. getCallerIdentityResponse_account From 79ee23c2d6de0b09bf6ef7a28ae88f1f8cb006e8 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 6 Sep 2023 10:37:14 -0400 Subject: [PATCH 086/187] Add OneOrListOf, use for Action{run} Sometimes it's useful to do more than one thing on an event, such as create and migrate the DB on post-deploy of a DB-related resource. Specifying a list for `run` is simpler, with more obvious semantics, than specifying multiple actions with the same `on`. However, to avoid breaking existing specifications specifying a single item for `run` should still be supported. The `OneOrListOf` wrapper implements parsing such values either way either. And it's `Foldable` instance makes usage very nice. The `Semigroup` instance is not currently used, so I'm open to removing it, but it seemed reasonable to include. --- src/Stackctl/Action.hs | 10 +++--- src/Stackctl/OneOrListOf.hs | 56 ++++++++++++++++++++++++++++++ stackctl.cabal | 2 ++ test/Stackctl/OneOrListOfSpec.hs | 47 +++++++++++++++++++++++++ test/Stackctl/StackSpecYamlSpec.hs | 2 +- 5 files changed, 112 insertions(+), 5 deletions(-) create mode 100644 src/Stackctl/OneOrListOf.hs create mode 100644 test/Stackctl/OneOrListOfSpec.hs diff --git a/src/Stackctl/Action.hs b/src/Stackctl/Action.hs index 294db29..8df2a4c 100644 --- a/src/Stackctl/Action.hs +++ b/src/Stackctl/Action.hs @@ -25,16 +25,18 @@ import Data.Aeson import Data.List (find) import Stackctl.AWS import Stackctl.AWS.Lambda +import Stackctl.OneOrListOf +import qualified Stackctl.OneOrListOf as OneOrListOf data Action = Action { on :: ActionOn - , run :: ActionRun + , run :: OneOrListOf ActionRun } deriving stock (Eq, Show, Generic) deriving anyclass (FromJSON, ToJSON) -newAction :: ActionOn -> ActionRun -> Action -newAction = Action +newAction :: ActionOn -> [ActionRun] -> Action +newAction on runs = Action {on, run = OneOrListOf.fromList runs} data ActionOn = PostDeploy deriving stock (Eq, Show, Generic) @@ -97,7 +99,7 @@ runAction runAction stackName Action {on, run} = do logInfo $ "Running action" :# ["on" .= on, "run" .= run] - case run of + for_ run $ \case InvokeLambdaByStackOutput outputName -> do outputs <- awsCloudFormationDescribeStackOutputs stackName case findOutputValue outputName outputs of diff --git a/src/Stackctl/OneOrListOf.hs b/src/Stackctl/OneOrListOf.hs new file mode 100644 index 0000000..0c4d401 --- /dev/null +++ b/src/Stackctl/OneOrListOf.hs @@ -0,0 +1,56 @@ +module Stackctl.OneOrListOf + ( OneOrListOf + , fromList + ) where + +import Stackctl.Prelude + +import Data.Aeson + +-- | Type representing one @a@ or a list of @a@ +-- +-- This type is isomorphic both @'NonEmpty' a@ and @'Either' a [a]@. Its primary +-- use-case is to parse Yaml (through its 'FromJSON') where users may specify a +-- list of values, but specifying a single value is worth supporting, typically +-- for backwards-compatibility: +-- +-- @ +-- something: +-- field: +-- - one +-- - two +-- +-- something: +-- field: one # => should be treated like field: [one] +-- @ +-- +-- The 'Foldable' instance should be used to treat the value like a list, such +-- as extracting it directly via 'toList'. +-- +-- Implementation note: this type preserves the form in which it was decoded (in +-- other words, it's not a @newtype@ over one of the isomorphic types mentioned +-- above), so that we can encode it back out in the same format. +data OneOrListOf a = One a | List [a] + deriving stock (Eq, Show, Generic, Foldable) + +fromList :: [a] -> OneOrListOf a +fromList = List + +instance Semigroup (OneOrListOf a) where + One a <> One b = List [a, b] + One a <> List bs = List $ a : bs + List as <> One b = List $ as <> [b] + List as <> List bs = List $ as <> bs + +instance FromJSON a => FromJSON (OneOrListOf a) where + parseJSON = \case + Array xs -> List . toList <$> traverse parseJSON xs + v -> One <$> parseJSON v + +instance ToJSON a => ToJSON (OneOrListOf a) where + toJSON = \case + One a -> toJSON a + List as -> toJSON as + toEncoding = \case + One a -> toEncoding a + List as -> toEncoding as diff --git a/stackctl.cabal b/stackctl.cabal index 7d730e3..7f31f25 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -43,6 +43,7 @@ library Stackctl.Config.RequiredVersion Stackctl.DirectoryOption Stackctl.FilterOption + Stackctl.OneOrListOf Stackctl.Options Stackctl.ParameterOption Stackctl.Prelude @@ -185,6 +186,7 @@ test-suite spec Stackctl.Config.RequiredVersionSpec Stackctl.ConfigSpec Stackctl.FilterOptionSpec + Stackctl.OneOrListOfSpec Stackctl.Spec.Changes.FormatSpec Stackctl.StackDescriptionSpec Stackctl.StackSpecSpec diff --git a/test/Stackctl/OneOrListOfSpec.hs b/test/Stackctl/OneOrListOfSpec.hs new file mode 100644 index 0000000..4261c89 --- /dev/null +++ b/test/Stackctl/OneOrListOfSpec.hs @@ -0,0 +1,47 @@ +module Stackctl.OneOrListOfSpec + ( spec + ) where + +import Stackctl.Prelude + +import Data.Aeson +import qualified Data.Yaml as Yaml +import Stackctl.OneOrListOf +import Test.Hspec + +data ExampleObject = ExampleObject + { oneOf :: OneOrListOf Text + , listOf :: OneOrListOf Text + } + deriving stock (Generic) + deriving anyclass (FromJSON, ToJSON) + +-- N.B. the sorting and indentation must match what encode will do in order for +-- the round-trip spec to pass. +exampleBS :: ByteString +exampleBS = + mconcat + [ "listOf:\n" + , "- one\n" + , "- two\n" + , "oneOf: one\n" + ] + +spec :: Spec +spec = do + it "Foldable" $ do + ExampleObject {..} <- Yaml.decodeThrow exampleBS + + toList oneOf `shouldBe` ["one"] + + toList listOf `shouldBe` ["one", "two"] + + it "Semigroup" $ do + ExampleObject {..} <- Yaml.decodeThrow exampleBS + + toList (oneOf <> listOf) `shouldBe` ["one", "one", "two"] + + it "From/ToJSON" $ do + decoded <- Yaml.decodeThrow @_ @ExampleObject exampleBS + + Yaml.encode decoded `shouldBe` exampleBS diff --git a/test/Stackctl/StackSpecYamlSpec.hs b/test/Stackctl/StackSpecYamlSpec.hs index 91942dc..e67ead3 100644 --- a/test/Stackctl/StackSpecYamlSpec.hs +++ b/test/Stackctl/StackSpecYamlSpec.hs @@ -24,7 +24,7 @@ spec = do , ssyDepends = Just [StackName "a-stack", StackName "another-stack"] , ssyActions = Just - [newAction PostDeploy $ InvokeLambdaByName "a-lambda"] + [newAction PostDeploy [InvokeLambdaByName "a-lambda"]] , ssyParameters = Just $ parametersYaml From 77636e129066c0a8da7f9184469959d78dc7896a Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 6 Sep 2023 10:49:59 -0400 Subject: [PATCH 087/187] Add Exec and Shell to ActionRun constructors This can be used to execute an arbitrary process on events such as `PostDeploy`. The distinct constructors (vs parsing differently to a single constructor) are used to ensure that our Yaml round-trips faithfully. --- src/Stackctl/Action.hs | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/src/Stackctl/Action.hs b/src/Stackctl/Action.hs index 8df2a4c..585e2df 100644 --- a/src/Stackctl/Action.hs +++ b/src/Stackctl/Action.hs @@ -21,12 +21,15 @@ module Stackctl.Action import Stackctl.Prelude hiding (on) +import Blammo.Logging.Logger (flushLogger) import Data.Aeson import Data.List (find) +import qualified Data.List.NonEmpty as NE import Stackctl.AWS import Stackctl.AWS.Lambda import Stackctl.OneOrListOf import qualified Stackctl.OneOrListOf as OneOrListOf +import System.Process.Typed data Action = Action { on :: ActionOn @@ -56,31 +59,45 @@ instance ToJSON ActionOn where data ActionRun = InvokeLambdaByStackOutput Text | InvokeLambdaByName Text + | Exec (NonEmpty String) + | Shell String deriving stock (Eq, Show) instance FromJSON ActionRun where parseJSON = withObject "ActionRun" $ \o -> (InvokeLambdaByStackOutput <$> o .: "InvokeLambdaByStackOutput") <|> (InvokeLambdaByName <$> o .: "InvokeLambdaByName") + <|> (Exec <$> o .: "Exec") + <|> (Shell <$> o .: "Shell") instance ToJSON ActionRun where toJSON = object . \case InvokeLambdaByStackOutput name -> ["InvokeLambdaByStackOutput" .= name] InvokeLambdaByName name -> ["InvokeLambdaByName" .= name] + Exec args -> ["Exec" .= args] + Shell arg -> ["Shell" .= arg] toEncoding = pairs . \case InvokeLambdaByStackOutput name -> "InvokeLambdaByStackOutput" .= name InvokeLambdaByName name -> "InvokeLambdaByName" .= name + Exec args -> "Exec" .= args + Shell arg -> "Shell" .= arg data ActionFailure = NoSuchOutput | InvokeLambdaFailure + | ExecFailure ExitCode deriving stock (Show) deriving anyclass (Exception) runActions - :: (MonadResource m, MonadLogger m, MonadReader env m, HasAwsEnv env) + :: ( MonadResource m + , MonadLogger m + , MonadReader env m + , HasLogger env + , HasAwsEnv env + ) => StackName -> ActionOn -> [Action] @@ -92,7 +109,12 @@ shouldRunOn :: Action -> ActionOn -> Bool shouldRunOn Action {on} on' = on == on' runAction - :: (MonadResource m, MonadLogger m, MonadReader env m, HasAwsEnv env) + :: ( MonadResource m + , MonadLogger m + , MonadReader env m + , HasLogger env + , HasAwsEnv env + ) => StackName -> Action -> m () @@ -113,6 +135,8 @@ runAction stackName Action {on, run} = do throwIO NoSuchOutput Just name -> invoke name InvokeLambdaByName name -> invoke name + Exec args -> execProcessAction (NE.head args) (NE.tail args) + Shell arg -> execProcessAction "sh" ["-c", arg] where invoke name = do result <- awsLambdaInvoke name payload @@ -124,3 +148,15 @@ runAction stackName Action {on, run} = do findOutputValue :: Text -> [Output] -> Maybe Text findOutputValue name = view output_outputValue <=< find ((== Just name) . view output_outputKey) + +execProcessAction + :: (MonadIO m, MonadLogger m, MonadReader env m, HasLogger env) + => String + -> [String] + -> m () +execProcessAction cmd args = do + logDebug $ "runProcess" :# ["command" .= (cmd : args)] + flushLogger + + ec <- runProcess $ proc cmd args + unless (ec == ExitSuccess) $ throwIO $ ExecFailure ec From edc58c0c6b04fd157559f51eb75d64a30c8919fb Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 6 Sep 2023 13:20:05 -0400 Subject: [PATCH 088/187] Document new Actions[].run constructors --- man/stackctl.1.ronn | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/man/stackctl.1.ronn b/man/stackctl.1.ronn index fd6782d..3f86fa2 100644 --- a/man/stackctl.1.ronn +++ b/man/stackctl.1.ronn @@ -124,13 +124,22 @@ And these constituent parts are used as follows: **PostDeploy**: run the action after a successful deployment. * `{.Actions[].run}`: - The action to perform on the given event: + An action or list of actions to perform on the given event: **InvokeLambdaByStackOutput**: : invoke the function whose name is found in the given Output of the deployed Stack. **InvokeLambdaByName**: : invoke the given function. + **Exec**: [, ]: execute the given `command` and + `argument`s. + + **Shell**: : execute the given argument via `sh -c`. + + Executed processes will inherit any environment variables and print their + own `stdout` and `stderr`. If they do not exit 0, an exception is thrown and + `stackctl` itself exits. + * `{.Parameters}`: Optional. Parameters to use when deploying the Stack. From b7f072fb96e90fbd8ee21ae4bdc9b1220eaba6ee Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Fri, 8 Sep 2023 11:20:22 -0400 Subject: [PATCH 089/187] Silence another new warning --- package.yaml | 1 + stackctl.cabal | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/package.yaml b/package.yaml index 3e9eef0..43ba6fb 100644 --- a/package.yaml +++ b/package.yaml @@ -20,6 +20,7 @@ ghc-options: - -fwrite-ide-info - -Weverything - -Wno-all-missed-specialisations + - -Wno-missed-specialisations - -Wno-missing-import-lists - -Wno-missing-kind-signatures - -Wno-missing-local-signatures diff --git a/stackctl.cabal b/stackctl.cabal index 7f31f25..3162f41 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -96,7 +96,7 @@ library StandaloneDeriving TypeApplications TypeFamilies - ghc-options: -fignore-optim-changes -fwrite-ide-info -Weverything -Wno-all-missed-specialisations -Wno-missing-import-lists -Wno-missing-kind-signatures -Wno-missing-local-signatures -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module -Wno-unsafe -optP-Wno-nonportable-include-path + ghc-options: -fignore-optim-changes -fwrite-ide-info -Weverything -Wno-all-missed-specialisations -Wno-missed-specialisations -Wno-missing-import-lists -Wno-missing-kind-signatures -Wno-missing-local-signatures -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module -Wno-unsafe -optP-Wno-nonportable-include-path build-depends: Blammo >=1.1.2.1 , Glob @@ -171,7 +171,7 @@ executable stackctl StandaloneDeriving TypeApplications TypeFamilies - ghc-options: -fignore-optim-changes -fwrite-ide-info -Weverything -Wno-all-missed-specialisations -Wno-missing-import-lists -Wno-missing-kind-signatures -Wno-missing-local-signatures -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module -Wno-unsafe -optP-Wno-nonportable-include-path -threaded -rtsopts -with-rtsopts=-N + ghc-options: -fignore-optim-changes -fwrite-ide-info -Weverything -Wno-all-missed-specialisations -Wno-missed-specialisations -Wno-missing-import-lists -Wno-missing-kind-signatures -Wno-missing-local-signatures -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module -Wno-unsafe -optP-Wno-nonportable-include-path -threaded -rtsopts -with-rtsopts=-N build-depends: base ==4.* , stackctl @@ -220,7 +220,7 @@ test-suite spec StandaloneDeriving TypeApplications TypeFamilies - ghc-options: -fignore-optim-changes -fwrite-ide-info -Weverything -Wno-all-missed-specialisations -Wno-missing-import-lists -Wno-missing-kind-signatures -Wno-missing-local-signatures -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module -Wno-unsafe -optP-Wno-nonportable-include-path + ghc-options: -fignore-optim-changes -fwrite-ide-info -Weverything -Wno-all-missed-specialisations -Wno-missed-specialisations -Wno-missing-import-lists -Wno-missing-kind-signatures -Wno-missing-local-signatures -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module -Wno-unsafe -optP-Wno-nonportable-include-path build-depends: Glob , QuickCheck From b63e0bb1a13b57ea060b36a90bfb228b364f74fe Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Fri, 8 Sep 2023 11:20:28 -0400 Subject: [PATCH 090/187] Use awsSilently for change-set creation We are capturing and reporting the errors ourselves, so we don't need or want the redundant AWS logging. --- src/Stackctl/AWS/CloudFormation.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Stackctl/AWS/CloudFormation.hs b/src/Stackctl/AWS/CloudFormation.hs index 61282ff..959afb0 100644 --- a/src/Stackctl/AWS/CloudFormation.hs +++ b/src/Stackctl/AWS/CloudFormation.hs @@ -381,6 +381,7 @@ awsCloudFormationCreateChangeSet awsCloudFormationCreateChangeSet stackName mStackDescription stackTemplate parameters capabilities tags = fmap (first formatServiceError) $ trying (_ServiceError . hasStatus 400) + $ awsSilently $ do name <- newChangeSetName From 6ab3fe899977382f06a8f73f26d7366336c17c48 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Fri, 8 Sep 2023 12:45:16 -0400 Subject: [PATCH 091/187] Version bump --- CHANGELOG.md | 20 +++++++++++++++++++- package.yaml | 2 +- stackctl.cabal | 2 +- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 890af36..13cbfae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,22 @@ -## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.4.4.0...main) +## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.5.0.0...main) + +## [v1.4.4.1](https://github.com/freckle/stackctl/compare/v1.4.4.0...v1.5.0.0) + +Breaking changes: + +- Don't require a name argument to the `awsSimple` function + +New features: + +- Add `Exec` and `Shell` features in `actions[].run` +- Support lists in `actions[].run` (single items still work) +- Add more granular status indicators in `stack-ls(1)` output, print a legend of + these indicators as a footer (disable with `--no-legend`) + +Fixes: + +- Fix for redundant change-set creation errors in logging output +- Fix globbing bug in auto-expansion of `--filter` arguments ## [v1.4.4.0](https://github.com/freckle/stackctl/compare/v1.4.3.0...v1.4.4.0) diff --git a/package.yaml b/package.yaml index 43ba6fb..d921698 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: stackctl -version: 1.4.4.0 +version: 1.5.0.0 github: freckle/stackctl license: MIT author: Freckle Engineering diff --git a/stackctl.cabal b/stackctl.cabal index 3162f41..a9ffc53 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -5,7 +5,7 @@ cabal-version: 1.18 -- see: https://github.com/sol/hpack name: stackctl -version: 1.4.4.0 +version: 1.5.0.0 description: Please see homepage: https://github.com/freckle/stackctl#readme bug-reports: https://github.com/freckle/stackctl/issues From 48f6c37f85a44923335bb6724add86d9262a1d56 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Mon, 11 Sep 2023 09:05:36 -0400 Subject: [PATCH 092/187] Fix CHANGELOG header --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13cbfae..84a53fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.5.0.0...main) -## [v1.4.4.1](https://github.com/freckle/stackctl/compare/v1.4.4.0...v1.5.0.0) +## [v1.5.0.0](https://github.com/freckle/stackctl/compare/v1.4.4.0...v1.5.0.0) Breaking changes: From ed20ed3aca840bb74e3fac5225473b9258a77207 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Thu, 21 Sep 2023 16:51:24 -0400 Subject: [PATCH 093/187] Use requiredVersionToText in logged warning Before, this warning looked like: ``` 2023-09-28 14:39:53 [error ] Incompatible Stackctl version current=1.5.0.0 required=RequiredVersion {requiredVersionOp = RequiredVersionIsh, requiredVersionCompareWith = Version {versionBranch = [1,4], versionTags = []}} ``` Now, it looks like: ``` 2023-09-28 14:40:11 [error ] Incompatible Stackctl version current=1.5.0.0 required="=~ 1.4" ``` NOTE: the `show` may seem redundant now, but it was kept to make the value come out quoted (and escaped, if that's ever necessary). --- src/Stackctl/Config.hs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Stackctl/Config.hs b/src/Stackctl/Config.hs index 925a6ea..9cf2148 100644 --- a/src/Stackctl/Config.hs +++ b/src/Stackctl/Config.hs @@ -63,7 +63,8 @@ configErrorMessage = \case :# ["error" .= Yaml.prettyPrintParseException ex] ConfigInvalid errs -> "Invalid configuration" :# ["errors" .= errs] ConfigVersionNotSatisfied rv v -> - "Incompatible Stackctl version" :# ["current" .= v, "required" .= show rv] + "Incompatible Stackctl version" + :# ["current" .= v, "required" .= show (requiredVersionToText rv)] loadConfigOrExit :: (MonadIO m, MonadLogger m) => m Config loadConfigOrExit = either die pure =<< loadConfig From 316b2c8165cee4fd441b8e5bc033547773a9a1db Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Thu, 21 Sep 2023 16:51:24 -0400 Subject: [PATCH 094/187] Add warning if specs depend on unknown specs It's a common typo to add a stack in `Depends` that doesn't actually exist. When this happens, everything still "works", except multi-stack deploys may not order correctly. We'll now check for this and emit a warning. It may be better as a fatal error (I'm not sure I can come up with a real use-case for depending on a stack not also managed in this same set of specifications), but I'm starting out conservatively for now. ![Example](https://files.pbrisbin.com/screenshots/screenshot.667322.png) --- package.yaml | 1 + src/Stackctl/Spec/Discover.hs | 47 +++++++++++++++++++++++++++++++---- src/Stackctl/StackSpec.hs | 1 + stackctl.cabal | 1 + 4 files changed, 45 insertions(+), 5 deletions(-) diff --git a/package.yaml b/package.yaml index d921698..88c1b21 100644 --- a/package.yaml +++ b/package.yaml @@ -90,6 +90,7 @@ library: - rio - semigroups - text + - text-metrics - time - transformers - typed-process diff --git a/src/Stackctl/Spec/Discover.hs b/src/Stackctl/Spec/Discover.hs index 6fddb20..5f69188 100644 --- a/src/Stackctl/Spec/Discover.hs +++ b/src/Stackctl/Spec/Discover.hs @@ -6,8 +6,9 @@ module Stackctl.Spec.Discover import Stackctl.Prelude -import Data.List.Extra (dropPrefix) +import Data.List.Extra (dropPrefix, minimumBy) import qualified Data.List.NonEmpty as NE +import Data.Text.Metrics (levenshtein) import Stackctl.AWS import Stackctl.AWS.Scope import Stackctl.Config (HasConfig) @@ -64,10 +65,13 @@ discoverSpecs = do withThreadContext context $ do checkForDuplicateStackNames specPaths - specs <- - sortStackSpecs - . filterStackSpecs filterOption - <$> traverse (readStackSpec dir) specPaths + allSpecs <- traverse (readStackSpec dir) specPaths + + let + known = map stackSpecStackName allSpecs + specs = sortStackSpecs $ filterStackSpecs filterOption allSpecs + + traverse_ (checkForUnknownDepends known) specs when (null specs) $ logWarn "No specs found" specs <$ logDebug ("Discovered specs" :# ["matched" .= length specs]) @@ -94,6 +98,39 @@ checkForDuplicateStackNames = exitFailure +-- | Warn if a 'StackSpec' depends on a name not in the given 'StackName's +-- +-- The 'StackName's are built from all specs, but we only run this with specs +-- that are filtered in. +-- +-- NB. This function is written so it can easily be made into a fatal error +-- (like 'checkForDuplicateStackNames'), but we only warn for now. +checkForUnknownDepends :: MonadLogger m => [StackName] -> StackSpec -> m () +checkForUnknownDepends known spec = + traverse_ reportUnknownDepends + $ NE.nonEmpty + $ filter (`notElem` known) + $ stackSpecDepends spec + where + reportUnknownDepends depends = do + for_ depends $ \depend -> do + let (nearest, _distance) = + minimumBy (comparing snd) + . map (id &&& getDistance depend) + $ known + + logWarn + $ "Stack lists dependency that does not exist" + :# [ "dependency" + .= ( unStackName (stackSpecStackName spec) + <> " -> " + <> unStackName depend + ) + , "hint" .= ("Did you mean " <> unStackName nearest <> "?") + ] + + getDistance = levenshtein `on` unStackName + buildSpecPath :: (MonadReader env m, HasAwsScope env) => StackName diff --git a/src/Stackctl/StackSpec.hs b/src/Stackctl/StackSpec.hs index 8cc7c18..77cf477 100644 --- a/src/Stackctl/StackSpec.hs +++ b/src/Stackctl/StackSpec.hs @@ -5,6 +5,7 @@ module Stackctl.StackSpec , stackSpecSpecBody , stackSpecStackName , stackSpecStackDescription + , stackSpecDepends , stackSpecActions , stackSpecParameters , stackSpecCapabilities diff --git a/stackctl.cabal b/stackctl.cabal index a9ffc53..e6cd587 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -130,6 +130,7 @@ library , rio , semigroups , text + , text-metrics , time , transformers , typed-process From 02dbb57f82ddeb2bdcaaa539fe466fe0f4935a1c Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Thu, 28 Sep 2023 10:19:06 -0400 Subject: [PATCH 095/187] Handle no-or-missing specs more safely While filtering down to no specs is expected (and handled), having no specs at all is not expected. And we now use this pre-filtered list with `minimumBy` in `checkForUnknownDepends`. So if we found truly no specs at all, we would have an exception there trying to get the minimum of an empty list. I've made this mistake myself, *thinking* I was in our `infra` directory, running `stackctl` repeatedly with different forms of `--filter` that I thought were not doing what I expected, only to realize I'm in the wrong directory. Without this change, that would now be an even more confusing empty-list exception from the introduction of `minimumBy`. With this change, it's back to the no-op it was before, but now has an even clearer warning, which was added as an obvious consequence of thinking through the types at all and arriving at a `Nothing` that had to be handled. --- src/Stackctl/Spec/Discover.hs | 44 +++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/src/Stackctl/Spec/Discover.hs b/src/Stackctl/Spec/Discover.hs index 5f69188..3e6a937 100644 --- a/src/Stackctl/Spec/Discover.hs +++ b/src/Stackctl/Spec/Discover.hs @@ -6,8 +6,9 @@ module Stackctl.Spec.Discover import Stackctl.Prelude -import Data.List.Extra (dropPrefix, minimumBy) +import Data.List.Extra (dropPrefix) import qualified Data.List.NonEmpty as NE +import qualified Data.List.NonEmpty.Extra as NE import Data.Text.Metrics (levenshtein) import Stackctl.AWS import Stackctl.AWS.Scope @@ -65,16 +66,28 @@ discoverSpecs = do withThreadContext context $ do checkForDuplicateStackNames specPaths - allSpecs <- traverse (readStackSpec dir) specPaths - - let - known = map stackSpecStackName allSpecs - specs = sortStackSpecs $ filterStackSpecs filterOption allSpecs - - traverse_ (checkForUnknownDepends known) specs - - when (null specs) $ logWarn "No specs found" - specs <$ logDebug ("Discovered specs" :# ["matched" .= length specs]) + mAllSpecs <- NE.nonEmpty <$> traverse (readStackSpec dir) specPaths + + case mAllSpecs of + Nothing -> do + [] + <$ logWarn + ( "Missing or empty specification directory" + :# [ "directory" .= dir + , "hint" .= ("Is this the correct directory?" :: Text) + ] + ) + Just allSpecs -> do + let + known = stackSpecStackName <$> allSpecs + specs = + sortStackSpecs + $ filterStackSpecs filterOption + $ NE.toList allSpecs + + traverse_ (checkForUnknownDepends known) specs + when (null specs) $ logWarn "No specs matched filters" + specs <$ logDebug ("Discovered specs" :# ["matched" .= length specs]) checkForDuplicateStackNames :: (MonadIO m, MonadLogger m) => [StackSpecPath] -> m () @@ -105,7 +118,8 @@ checkForDuplicateStackNames = -- -- NB. This function is written so it can easily be made into a fatal error -- (like 'checkForDuplicateStackNames'), but we only warn for now. -checkForUnknownDepends :: MonadLogger m => [StackName] -> StackSpec -> m () +checkForUnknownDepends + :: MonadLogger m => NonEmpty StackName -> StackSpec -> m () checkForUnknownDepends known spec = traverse_ reportUnknownDepends $ NE.nonEmpty @@ -115,9 +129,9 @@ checkForUnknownDepends known spec = reportUnknownDepends depends = do for_ depends $ \depend -> do let (nearest, _distance) = - minimumBy (comparing snd) - . map (id &&& getDistance depend) - $ known + NE.minimumBy1 (comparing snd) + $ (id &&& getDistance depend) + <$> known logWarn $ "Stack lists dependency that does not exist" From 2e3314118574a73471d7fa459d147b0a38735027 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 3 Oct 2023 11:15:18 -0400 Subject: [PATCH 096/187] Version bump --- CHANGELOG.md | 8 +++++++- package.yaml | 2 +- stackctl.cabal | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84a53fb..6f7691d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,10 @@ -## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.5.0.0...main) +## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.5.0.1...main) + +## [v1.5.0.1](https://github.com/freckle/stackctl/compare/v1.5.0.0...v1.5.0.1) + +- Handle missing-or-empty specs directory more explicitly +- Add warning for `Depends` pointing to non-existent spec +- Fix formatting of required version in warning message ## [v1.5.0.0](https://github.com/freckle/stackctl/compare/v1.4.4.0...v1.5.0.0) diff --git a/package.yaml b/package.yaml index 88c1b21..e0618bc 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: stackctl -version: 1.5.0.0 +version: 1.5.0.1 github: freckle/stackctl license: MIT author: Freckle Engineering diff --git a/stackctl.cabal b/stackctl.cabal index e6cd587..4de63c6 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -5,7 +5,7 @@ cabal-version: 1.18 -- see: https://github.com/sol/hpack name: stackctl -version: 1.5.0.0 +version: 1.5.0.1 description: Please see homepage: https://github.com/freckle/stackctl#readme bug-reports: https://github.com/freckle/stackctl/issues From 5b4342b83e290585bc6e00b4b37668a5d28d765e Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Fri, 26 May 2023 09:42:37 -0400 Subject: [PATCH 097/187] Move to a different release action The way things were before we created a (pulished) release first, then build and uploaded assets. This meant (particularly with OSX) the release existed as "latest" for a long time without assets. This causes issues for anyone that tries to install during this time, including some test suites. This new action allows you to create a Draft release, upload assets, then publish it -- meaning it won't be visible until it's ready, solving that problem. Annoyingly, we had to fork the action to do it, as the best one our there needed a fix and the maintainer is gone. --- .github/PULL_REQUEST_TEMPLATE.md | 15 +++++++++ .github/release.yml | 11 +++++++ .github/workflows/release.yml | 54 +++++++++++++++----------------- 3 files changed, 52 insertions(+), 28 deletions(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/release.yml diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..0cd1e88 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,15 @@ +_Replace this content with your own_ + +## Checklists + +### About the changes + +- [ ] Tests added if necessary +- [ ] man-pages (`./man`) updated if necessary +- [ ] Formatted properly (e.g. Restyled passes) + +### About the PR + +- [ ] Descriptive, imperative-tense title +- [ ] Body explaining the _why_ of the change +- [ ] `breaking-change` or `enhancement` label applied if appropriate diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..4c9363d --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,11 @@ +changelog: + categories: + - title: Breaking Changes + labels: + - breaking-change + - title: Features + labels: + - enhancement + - title: Other Changes + labels: + - "*" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 010f17a..8af3273 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: Release executables +name: Release on: push: @@ -8,11 +8,9 @@ jobs: tag: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - id: tag uses: freckle/haskell-tag-action@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} outputs: tag: ${{ steps.tag.outputs.tag }} @@ -21,23 +19,14 @@ jobs: if: needs.tag.outputs.tag runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - id: release-notes - uses: freckle/release-notes-action@v1 - with: - version: ${{ needs.tag.outputs.tag }} - - uses: actions/create-release@v1 - id: create-release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - id: create-release + uses: freckle/action-gh-release@v2 with: tag_name: ${{ needs.tag.outputs.tag }} - release_name: Release ${{ needs.tag.outputs.tag }} - body_path: ${{ steps.release-notes.outputs.path }} - draft: false - prerelease: false + generate_release_notes: true + draft: true outputs: - upload_url: ${{ steps.create-release.outputs.upload_url }} + release_id: ${{ steps.create-release.outputs.id }} upload-assets: needs: create-release @@ -52,22 +41,31 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: freckle/stack-cache-action@v2 - run: echo "$HOME/.local/share/gem/ruby/3.0.0/bin" >>"$GITHUB_PATH" - run: gem install --user ronn-ng - if: ${{ runner.os == 'macOS' }} run: brew install coreutils # need GNU install - - run: make install.check - - uses: actions/upload-release-asset@v1 - id: upload-release-asset - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - run: | + make install.check # creates dist/stackctl.tar.gz + cp -v dist/stackctl.tar.gz stackctl-${{ matrix.suffix }}.tar.gz + - uses: freckle/action-gh-release@v2 + with: + id: ${{ needs.create-release.outputs.release_id }} + files: "*-${{ matrix.suffix }}.tar.gz" + fail_on_unmatched_files: true + + publish-release: + needs: + - create-release + - upload-assets + runs-on: ubuntu-latest + steps: + - uses: freckle/action-gh-release@v2 with: - upload_url: ${{ needs.create-release.outputs.upload_url }} - asset_path: ./dist/stackctl.tar.gz - asset_name: stackctl-${{ matrix.suffix }}.tar.gz - asset_content_type: application/gzip + id: ${{ needs.create-release.outputs.release_id }} + draft: false upload-hackage: needs: tag From 32aa7dc91f2f4aee9da4f15714ab046a40f21715 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Mon, 18 Sep 2023 07:49:47 -0400 Subject: [PATCH 098/187] Move to amazonka-mtl The `Stackctl.AWS` was inspiration for an OSS library that was just blocked on amazonka-2.0 release. Now that that's out, we released ours and can move to it. --- package.yaml | 1 + src/Stackctl/AWS/CloudFormation.hs | 68 ++++++----- src/Stackctl/AWS/Core.hs | 186 +++++++++-------------------- src/Stackctl/AWS/EC2.hs | 6 +- src/Stackctl/AWS/Lambda.hs | 12 +- src/Stackctl/AWS/STS.hs | 7 +- src/Stackctl/AWS/Scope.hs | 3 +- src/Stackctl/Action.hs | 8 +- src/Stackctl/AutoSSO.hs | 2 +- src/Stackctl/CLI.hs | 17 +-- src/Stackctl/RemovedStack.hs | 7 +- src/Stackctl/Spec/Capture.hs | 3 +- src/Stackctl/Spec/Cat.hs | 4 +- src/Stackctl/Spec/Changes.hs | 3 +- src/Stackctl/Spec/Deploy.hs | 21 ++-- src/Stackctl/Spec/Discover.hs | 8 +- src/Stackctl/Spec/List.hs | 3 +- src/Stackctl/StackSpec.hs | 4 +- stack.yaml | 1 + stack.yaml.lock | 7 ++ stackctl.cabal | 1 + 21 files changed, 151 insertions(+), 221 deletions(-) diff --git a/package.yaml b/package.yaml index e0618bc..a0b7ffb 100644 --- a/package.yaml +++ b/package.yaml @@ -70,6 +70,7 @@ library: - amazonka-core >= 2.0 - amazonka-ec2 >= 2.0 - amazonka-lambda >= 2.0 + - amazonka-mtl - amazonka-sso >= 2.0 - amazonka-sts >= 2.0 - bytestring diff --git a/src/Stackctl/AWS/CloudFormation.hs b/src/Stackctl/AWS/CloudFormation.hs index 959afb0..0a5ecc9 100644 --- a/src/Stackctl/AWS/CloudFormation.hs +++ b/src/Stackctl/AWS/CloudFormation.hs @@ -86,6 +86,7 @@ import Amazonka.Core , _MatchServiceError , _ServiceError ) +import qualified Amazonka.Env as Amazonka import Amazonka.Waiter (Accept (..)) import Conduit import Control.Lens ((?~)) @@ -97,7 +98,7 @@ import qualified Data.Text as T import Data.Time (UTCTime, defaultTimeLocale, formatTime, getCurrentTime) import qualified Data.UUID as UUID import qualified Data.UUID.V4 as UUID -import Stackctl.AWS.Core +import Stackctl.AWS.Core as AWS import Stackctl.Sort import Stackctl.StackDescription import System.FilePath.Glob @@ -172,16 +173,16 @@ newChangeSetName = liftIO $ do pure $ ChangeSetName $ T.intercalate "-" $ map pack parts awsCloudFormationDescribeStack - :: (MonadResource m, MonadReader env m, HasAwsEnv env) => StackName -> m Stack + :: (MonadIO m, MonadAWS m) => StackName -> m Stack awsCloudFormationDescribeStack stackName = do let req = newDescribeStacks & describeStacks_stackName ?~ unStackName stackName - awsSimple req $ \resp -> do + AWS.simple req $ \resp -> do stacks <- resp ^. describeStacksResponse_stacks listToMaybe stacks awsCloudFormationDescribeStackMaybe - :: (MonadUnliftIO m, MonadResource m, MonadReader env m, HasAwsEnv env) + :: (MonadUnliftIO m, MonadAWS m) => StackName -> m (Maybe Stack) awsCloudFormationDescribeStackMaybe stackName = @@ -193,7 +194,7 @@ awsCloudFormationDescribeStackMaybe stackName = <$> awsCloudFormationDescribeStack stackName awsCloudFormationDescribeStackOutputs - :: (MonadResource m, MonadReader env m, HasAwsEnv env) + :: (MonadIO m, MonadAWS m) => StackName -> m [Output] awsCloudFormationDescribeStackOutputs stackName = do @@ -201,7 +202,7 @@ awsCloudFormationDescribeStackOutputs stackName = do pure $ fromMaybe [] $ outputs stack awsCloudFormationDescribeStackEvents - :: (MonadResource m, MonadReader env m, HasAwsEnv env) + :: (MonadIO m, MonadAWS m) => StackName -> Maybe Text -- ^ Last-seen Id @@ -213,21 +214,21 @@ awsCloudFormationDescribeStackEvents stackName mLastId = do ?~ unStackName stackName runConduit - $ awsPaginate req + $ AWS.paginate req .| mapC (fromMaybe [] . (^. describeStackEventsResponse_stackEvents)) .| concatC .| takeWhileC (\e -> Just (e ^. stackEvent_eventId) /= mLastId) .| sinkList awsCloudFormationGetStackNamesMatching - :: (MonadResource m, MonadReader env m, HasAwsEnv env) + :: (MonadIO m, MonadAWS m) => Pattern -> m [StackName] awsCloudFormationGetStackNamesMatching p = do let req = newListStacks & listStacks_stackStatusFilter ?~ runningStatuses runConduit - $ awsPaginate req + $ AWS.paginate req .| concatMapC (^. listStacksResponse_stackSummaries) .| concatC .| mapC (^. stackSummary_stackName) @@ -236,7 +237,7 @@ awsCloudFormationGetStackNamesMatching p = do .| sinkList awsCloudFormationGetMostRecentStackEventId - :: (MonadResource m, MonadReader env m, HasAwsEnv env) + :: (MonadIO m, MonadAWS m) => StackName -> m (Maybe Text) awsCloudFormationGetMostRecentStackEventId stackName = do @@ -252,14 +253,14 @@ awsCloudFormationGetMostRecentStackEventId stackName = do [] -> Nothing (e : _) -> Just $ e ^. stackEvent_eventId - awsSimple req + AWS.simple req $ pure . getFirstEventId . fromMaybe [] . (^. describeStackEventsResponse_stackEvents) awsCloudFormationDeleteStack - :: (MonadResource m, MonadLogger m, MonadReader env m, HasAwsEnv env) + :: (MonadIO m, MonadLogger m, MonadAWS m) => StackName -> m StackDeleteResult awsCloudFormationDeleteStack stackName = do @@ -268,25 +269,25 @@ awsCloudFormationDeleteStack stackName = do describeReq = newDescribeStacks & describeStacks_stackName ?~ unStackName stackName - awsSimple deleteReq $ const $ pure () + AWS.simple deleteReq $ const $ pure () logDebug "Awaiting DeleteStack" - stackDeleteResult <$> awsAwait newStackDeleteComplete describeReq + stackDeleteResult <$> AWS.await newStackDeleteComplete describeReq awsCloudFormationWait - :: (MonadUnliftIO m, MonadResource m, MonadReader env m, HasAwsEnv env) + :: (MonadUnliftIO m, MonadAWS m) => StackName -> m StackDeployResult awsCloudFormationWait stackName = do either stackCreateResult stackUpdateResult <$> race - (awsAwait newStackCreateComplete req) - (awsAwait newStackUpdateComplete req) + (AWS.await newStackCreateComplete req) + (AWS.await newStackUpdateComplete req) where req = newDescribeStacks & describeStacks_stackName ?~ unStackName stackName awsCloudFormationGetTemplate - :: (MonadResource m, MonadReader env m, HasAwsEnv env) => StackName -> m Value + :: (MonadIO m, MonadAWS m) => StackName -> m Value awsCloudFormationGetTemplate stackName = do let req = @@ -298,7 +299,7 @@ awsCloudFormationGetTemplate stackName = do decodeTemplateBody body = fromMaybe (toJSON body) $ decodeStrict $ encodeUtf8 body - awsSimple req $ \resp -> do + AWS.simple req $ \resp -> do body <- resp ^. getTemplateResponse_templateBody pure $ decodeTemplateBody body @@ -366,10 +367,8 @@ changeSetFailed = (== ChangeSetStatus_FAILED) . csStatus awsCloudFormationCreateChangeSet :: ( MonadUnliftIO m - , MonadResource m , MonadLogger m - , MonadReader env m - , HasAwsEnv env + , MonadAWS m ) => StackName -> Maybe StackDescription @@ -410,22 +409,22 @@ awsCloudFormationCreateChangeSet stackName mStackDescription stackTemplate param logInfo $ "Creating changeset..." :# ["name" .= name, "type" .= changeSetType] - csId <- awsSimple req (^. createChangeSetResponse_id) + csId <- AWS.simple req (^. createChangeSetResponse_id) logDebug "Awaiting CREATE_COMPLETE" - void $ awsAwait newChangeSetCreateComplete $ newDescribeChangeSet csId + void $ AWS.await newChangeSetCreateComplete $ newDescribeChangeSet csId logInfo "Retrieving changeset..." cs <- awsCloudFormationDescribeChangeSet $ ChangeSetId csId pure $ cs <$ guard (not $ changeSetFailed cs) awsCloudFormationDescribeChangeSet - :: (MonadResource m, MonadReader env m, HasAwsEnv env) + :: (MonadIO m, MonadAWS m) => ChangeSetId -> m ChangeSet awsCloudFormationDescribeChangeSet changeSetId = do let req = newDescribeChangeSet $ unChangeSetId changeSetId - awsSimple req changeSetFromResponse + AWS.simple req changeSetFromResponse sortChanges :: [Change] -> [Change] sortChanges = sortByDependencies changeName changeCausedBy @@ -445,18 +444,16 @@ detailCausingLogicalResourceId ResourceChangeDetail' {..} = T.takeWhile (/= '.') <$> causingEntity awsCloudFormationExecuteChangeSet - :: (MonadResource m, MonadReader env m, HasAwsEnv env) => ChangeSetId -> m () + :: (MonadIO m, MonadAWS m) => ChangeSetId -> m () awsCloudFormationExecuteChangeSet changeSetId = do - void $ awsSend $ newExecuteChangeSet $ unChangeSetId changeSetId + void $ AWS.send $ newExecuteChangeSet $ unChangeSetId changeSetId awsCloudFormationDeleteAllChangeSets - :: (MonadResource m, MonadLogger m, MonadReader env m, HasAwsEnv env) - => StackName - -> m () + :: (MonadIO m, MonadLogger m, MonadAWS m) => StackName -> m () awsCloudFormationDeleteAllChangeSets stackName = do logInfo "Deleting all changesets" runConduit - $ awsPaginate (newListChangeSets $ unStackName stackName) + $ AWS.paginate (newListChangeSets $ unStackName stackName) .| concatMapC ( \resp -> fromMaybe [] $ do ss <- resp ^. listChangeSetsResponse_summaries @@ -465,7 +462,7 @@ awsCloudFormationDeleteAllChangeSets stackName = do .| mapM_C ( \csId -> do logInfo $ "Enqueing delete" :# ["changeSetId" .= csId] - void $ awsSend $ newDeleteChangeSet csId + void $ AWS.send $ newDeleteChangeSet csId ) -- | Did we abandoned this Stack's first ever ChangeSet? @@ -509,3 +506,8 @@ runningStatuses = _ValidationError :: AsError a => Getting (First ServiceError) a ServiceError _ValidationError = _MatchServiceError defaultService "ValidationError" . hasStatus 400 + +awsSilently :: MonadAWS m => m a -> m a +awsSilently = AWS.localEnv $ Amazonka.env_logger .~ noop + where + noop _level _msg = pure () diff --git a/src/Stackctl/AWS/Core.hs b/src/Stackctl/AWS/Core.hs index 41587df..78b8931 100644 --- a/src/Stackctl/AWS/Core.hs +++ b/src/Stackctl/AWS/Core.hs @@ -1,95 +1,76 @@ module Stackctl.AWS.Core - ( AwsEnv - , HasAwsEnv (..) - , awsEnvDiscover - , awsWithAuth - , awsSimple - , awsSend - , awsPaginate - , awsAwait - , awsAssumeRole - - -- * Modifiers on 'AwsEnv' - , awsWithin - , awsTimeout - , awsSilently - - -- * 'Amazonka' extensions - , AccountId (..) + ( MonadAWS + , send + , paginate + , await + , withAuth + , localEnv + + -- * "Control.Monad.AWS" extensions + , simple + , discover + , assumeRole -- * Error-handling , handlingServiceError , formatServiceError - -- * 'Amazonka'/'ResourceT' re-exports + -- * "Amazonka" extensions + , AccountId (..) + + -- * "Amazonka" re-exports , Region (..) , FromText (..) , ToText (..) - , MonadResource ) where -import Stackctl.Prelude hiding (timeout) - -import Amazonka hiding (LogLevel (..)) -import qualified Amazonka as AWS +import Stackctl.Prelude + +import Amazonka + ( AWSRequest + , AWSResponse + , Region + , ServiceError + , serviceError_code + , serviceError_message + , serviceError_requestId + , _Sensitive + , _ServiceError + ) +import qualified Amazonka import Amazonka.Auth.Keys (fromSession) import Amazonka.Data.Text (FromText (..), ToText (..)) -import Amazonka.Env (env_auth, env_logger, env_region) +import qualified Amazonka.Env as Amazonka import Amazonka.STS.AssumeRole -import Conduit (ConduitM) +import Control.Monad.AWS import Control.Monad.Logger (defaultLoc, toLogStr) -import Control.Monad.Trans.Resource (MonadResource) import Data.Typeable (typeRep) import Stackctl.AWS.Orphans () import UnliftIO.Exception.Lens (handling) -newtype AwsEnv = AwsEnv - { unAwsEnv :: Env - } - -unL :: Lens' AwsEnv Env -unL = lens unAwsEnv $ \x y -> x {unAwsEnv = y} - -awsEnvDiscover :: MonadLoggerIO m => m AwsEnv -awsEnvDiscover = do - env <- liftIO $ newEnv discover - AwsEnv <$> configureLogging env - -configureLogging :: MonadLoggerIO m => Env -> m Env -configureLogging env = do +discover :: MonadLoggerIO m => m Amazonka.Env +discover = do + env <- liftIO $ Amazonka.newEnv Amazonka.discover loggerIO <- askLoggerIO let logger level = do loggerIO - defaultLoc -- TODO: there may be a way to get a CallStack/Loc + defaultLoc "Amazonka" ( case level of - AWS.Info -> LevelInfo - AWS.Error -> LevelError - AWS.Debug -> LevelDebug - AWS.Trace -> LevelOther "trace" + Amazonka.Info -> LevelInfo + Amazonka.Error -> LevelError + Amazonka.Debug -> LevelDebug + Amazonka.Trace -> LevelOther "trace" ) . toLogStr - pure $ env & env_logger .~ logger - -class HasAwsEnv env where - awsEnvL :: Lens' env AwsEnv - -instance HasAwsEnv AwsEnv where - awsEnvL = id + pure $ env & Amazonka.env_logger .~ logger -awsWithAuth - :: (MonadIO m, MonadReader env m, HasAwsEnv env) => (AuthEnv -> m a) -> m a -awsWithAuth f = do - auth <- view $ awsEnvL . unL . env_auth . to runIdentity - withAuth auth f - -awsSimple - :: forall a env m b +simple + :: forall a m b . ( HasCallStack - , MonadResource m - , MonadReader env m - , HasAwsEnv env + , MonadIO m + , MonadAWS m , AWSRequest a , Typeable a , Typeable (AWSResponse a) @@ -97,8 +78,8 @@ awsSimple => a -> (AWSResponse a -> Maybe b) -> m b -awsSimple req post = do - resp <- awsSend req +simple req post = do + resp <- send req let name = show $ typeRep $ Proxy @a @@ -106,53 +87,8 @@ awsSimple req post = do maybe (throwString err) pure $ post resp -awsSend - :: ( MonadResource m - , MonadReader env m - , HasAwsEnv env - , AWSRequest a - , Typeable a - , Typeable (AWSResponse a) - ) - => a - -> m (AWSResponse a) -awsSend req = do - AwsEnv env <- view awsEnvL - send env req - -awsPaginate - :: ( MonadResource m - , MonadReader env m - , HasAwsEnv env - , AWSPager a - , Typeable a - , Typeable (AWSResponse a) - ) - => a - -> ConduitM () (AWSResponse a) m () -awsPaginate req = do - AwsEnv env <- view awsEnvL - paginateEither env req >>= hoistEither - -hoistEither :: MonadIO m => Either Error a -> m a -hoistEither = either (liftIO . throwIO) pure - -awsAwait - :: ( MonadResource m - , MonadReader env m - , HasAwsEnv env - , AWSRequest a - , Typeable a - ) - => Wait a - -> a - -> m Accept -awsAwait w req = do - AwsEnv env <- view awsEnvL - await env w req - -awsAssumeRole - :: (MonadResource m, MonadReader env m, HasAwsEnv env) +assumeRole + :: (MonadIO m, MonadAWS m) => Text -- ^ Role ARN -> Text @@ -160,31 +96,21 @@ awsAssumeRole -> m a -- ^ Action to run as the assumed role -> m a -awsAssumeRole role sessionName f = do +assumeRole role sessionName f = do let req = newAssumeRole role sessionName - assumeEnv <- awsSimple req $ \resp -> do + assumeEnv <- simple req $ \resp -> do let creds = resp ^. assumeRoleResponse_credentials - token <- creds ^. authEnv_sessionToken + token <- creds ^. Amazonka.authEnv_sessionToken let - accessKeyId = creds ^. authEnv_accessKeyId - secretAccessKey = creds ^. authEnv_secretAccessKey . _Sensitive - - pure $ fromSession accessKeyId secretAccessKey $ token ^. _Sensitive - - local (awsEnvL . unL %~ assumeEnv) f - -awsWithin :: (MonadReader env m, HasAwsEnv env) => Region -> m a -> m a -awsWithin r = local $ awsEnvL . unL . env_region .~ r + accessKeyId = creds ^. Amazonka.authEnv_accessKeyId + secretAccessKey = creds ^. Amazonka.authEnv_secretAccessKey . _Sensitive + sessionToken = token ^. _Sensitive -awsTimeout :: (MonadReader env m, HasAwsEnv env) => Seconds -> m a -> m a -awsTimeout t = local $ awsEnvL . unL %~ globalTimeout t + pure $ fromSession accessKeyId secretAccessKey sessionToken -awsSilently :: (MonadReader env m, HasAwsEnv env) => m a -> m a -awsSilently = local $ awsEnvL . unL . env_logger .~ noop - where - noop _level _msg = pure () + localEnv assumeEnv f newtype AccountId = AccountId { unAccountId :: Text diff --git a/src/Stackctl/AWS/EC2.hs b/src/Stackctl/AWS/EC2.hs index 0a732f1..a41402b 100644 --- a/src/Stackctl/AWS/EC2.hs +++ b/src/Stackctl/AWS/EC2.hs @@ -6,13 +6,13 @@ import Stackctl.Prelude import Amazonka.EC2.DescribeAvailabilityZones import Amazonka.EC2.Types (AvailabilityZone (..)) -import Stackctl.AWS.Core +import Stackctl.AWS.Core as AWS awsEc2DescribeFirstAvailabilityZoneRegionName - :: (MonadResource m, MonadReader env m, HasAwsEnv env) => m Region + :: (MonadIO m, MonadAWS m) => m Region awsEc2DescribeFirstAvailabilityZoneRegionName = do let req = newDescribeAvailabilityZones - awsSimple req $ \resp -> do + AWS.simple req $ \resp -> do azs <- resp ^. describeAvailabilityZonesResponse_availabilityZones az <- listToMaybe azs rn <- regionName az diff --git a/src/Stackctl/AWS/Lambda.hs b/src/Stackctl/AWS/Lambda.hs index f111736..6df5ccc 100644 --- a/src/Stackctl/AWS/Lambda.hs +++ b/src/Stackctl/AWS/Lambda.hs @@ -10,10 +10,11 @@ module Stackctl.AWS.Lambda import Stackctl.Prelude hiding (trace) +import Amazonka (globalTimeout) import Amazonka.Lambda.Invoke import Data.Aeson import qualified Data.ByteString.Lazy as BSL -import Stackctl.AWS.Core +import Stackctl.AWS.Core as AWS data LambdaInvokeResult = LambdaInvokeSuccess ByteString @@ -63,10 +64,9 @@ data LambdaError = LambdaError deriving anyclass (FromJSON, ToJSON) awsLambdaInvoke - :: ( MonadResource m + :: ( MonadIO m , MonadLogger m - , MonadReader env m - , HasAwsEnv env + , MonadAWS m , ToJSON a ) => Text @@ -78,8 +78,8 @@ awsLambdaInvoke name payload = do -- Match Lambda's own limit (15 minutes) and add some buffer resp <- - awsTimeout 905 - $ awsSend + AWS.localEnv (globalTimeout 905) + $ AWS.send $ newInvoke name $ BSL.toStrict $ encode diff --git a/src/Stackctl/AWS/STS.hs b/src/Stackctl/AWS/STS.hs index 41b7538..0f8534c 100644 --- a/src/Stackctl/AWS/STS.hs +++ b/src/Stackctl/AWS/STS.hs @@ -5,10 +5,9 @@ module Stackctl.AWS.STS import Stackctl.Prelude import Amazonka.STS.GetCallerIdentity -import Stackctl.AWS.Core +import Stackctl.AWS.Core as AWS -awsGetCallerIdentityAccount - :: (MonadResource m, MonadReader env m, HasAwsEnv env) => m AccountId +awsGetCallerIdentityAccount :: (MonadIO m, MonadAWS m) => m AccountId awsGetCallerIdentityAccount = do - awsSimple newGetCallerIdentity $ \resp -> do + AWS.simple newGetCallerIdentity $ \resp -> do AccountId <$> resp ^. getCallerIdentityResponse_account diff --git a/src/Stackctl/AWS/Scope.hs b/src/Stackctl/AWS/Scope.hs index 6846ba9..3d3f72e 100644 --- a/src/Stackctl/AWS/Scope.hs +++ b/src/Stackctl/AWS/Scope.hs @@ -64,8 +64,7 @@ class HasAwsScope env where instance HasAwsScope AwsScope where awsScopeL = id -fetchAwsScope - :: (MonadResource m, MonadReader env m, HasAwsEnv env) => m AwsScope +fetchAwsScope :: (MonadIO m, MonadAWS m) => m AwsScope fetchAwsScope = AwsScope <$> awsGetCallerIdentityAccount diff --git a/src/Stackctl/Action.hs b/src/Stackctl/Action.hs index 585e2df..ffc496a 100644 --- a/src/Stackctl/Action.hs +++ b/src/Stackctl/Action.hs @@ -92,11 +92,11 @@ data ActionFailure deriving anyclass (Exception) runActions - :: ( MonadResource m + :: ( MonadIO m , MonadLogger m + , MonadAWS m , MonadReader env m , HasLogger env - , HasAwsEnv env ) => StackName -> ActionOn @@ -109,11 +109,11 @@ shouldRunOn :: Action -> ActionOn -> Bool shouldRunOn Action {on} on' = on == on' runAction - :: ( MonadResource m + :: ( MonadIO m , MonadLogger m + , MonadAWS m , MonadReader env m , HasLogger env - , HasAwsEnv env ) => StackName -> Action diff --git a/src/Stackctl/AutoSSO.hs b/src/Stackctl/AutoSSO.hs index cf20e3a..6d03a26 100644 --- a/src/Stackctl/AutoSSO.hs +++ b/src/Stackctl/AutoSSO.hs @@ -13,7 +13,7 @@ import Amazonka.SSO (_UnauthorizedException) import Data.Semigroup (Last (..)) import qualified Env import Options.Applicative -import Stackctl.AWS.Core (formatServiceError) +import Stackctl.AWS.Core as AWS (formatServiceError) import Stackctl.Prompt import System.Process.Typed import UnliftIO.Exception.Lens (catching) diff --git a/src/Stackctl/CLI.hs b/src/Stackctl/CLI.hs index 1bebca1..612eaa8 100644 --- a/src/Stackctl/CLI.hs +++ b/src/Stackctl/CLI.hs @@ -8,9 +8,11 @@ module Stackctl.CLI import Stackctl.Prelude import qualified Blammo.Logging.LogSettings.Env as LoggingEnv +import Control.Monad.AWS as AWS +import Control.Monad.AWS.ViaReader as AWS import Control.Monad.Catch (MonadCatch) -import Control.Monad.Trans.Resource (ResourceT, runResourceT) -import Stackctl.AWS +import Control.Monad.Trans.Resource (MonadResource, ResourceT, runResourceT) +import qualified Stackctl.AWS.Core as AWS import Stackctl.AWS.Scope import Stackctl.AutoSSO import Stackctl.ColorOption @@ -24,7 +26,7 @@ data App options = App , appConfig :: Config , appOptions :: options , appAwsScope :: AwsScope - , appAwsEnv :: AwsEnv + , appAwsEnv :: AWS.Env } optionsL :: Lens' (App options) options @@ -39,8 +41,8 @@ instance HasConfig (App options) where instance HasAwsScope (App options) where awsScopeL = lens appAwsScope $ \x y -> x {appAwsScope = y} -instance HasAwsEnv (App options) where - awsEnvL = lens appAwsEnv $ \x y -> x {appAwsEnv = y} +instance AWS.HasEnv (App options) where + envL = lens appAwsEnv $ \x y -> x {appAwsEnv = y} instance HasDirectoryOption options => HasDirectoryOption (App options) where directoryOptionL = optionsL . directoryOptionL @@ -73,6 +75,7 @@ newtype AppT app m a = AppT , MonadCatch , MonadMask ) + deriving (MonadAWS) via (ReaderAWS (AppT app m)) runAppT :: ( MonadMask m @@ -99,12 +102,12 @@ runAppT options f = do envLogSettings app <- runResourceT $ runLoggerLoggingT logger $ do - aws <- runReaderT (handleAutoSSO options awsEnvDiscover) logger + aws <- runReaderT (handleAutoSSO options AWS.discover) logger App logger <$> loadConfigOrExit <*> pure options - <*> runReaderT fetchAwsScope aws + <*> AWS.runEnvT fetchAwsScope aws <*> pure aws let diff --git a/src/Stackctl/RemovedStack.hs b/src/Stackctl/RemovedStack.hs index 3c3ef14..c55e05e 100644 --- a/src/Stackctl/RemovedStack.hs +++ b/src/Stackctl/RemovedStack.hs @@ -7,16 +7,15 @@ import Stackctl.Prelude import Control.Error.Util (hoistMaybe) import Control.Monad.Trans.Maybe (MaybeT (..), runMaybeT) import Stackctl.AWS.CloudFormation -import Stackctl.AWS.Core +import Stackctl.AWS.Core as AWS import Stackctl.AWS.Scope import Stackctl.FilterOption import UnliftIO.Directory (doesFileExist) inferRemovedStacks :: ( MonadUnliftIO m - , MonadResource m + , MonadAWS m , MonadReader env m - , HasAwsEnv env , HasAwsScope env , HasFilterOption env ) @@ -27,7 +26,7 @@ inferRemovedStacks = do catMaybes <$> traverse (findRemovedStack scope) paths findRemovedStack - :: (MonadUnliftIO m, MonadResource m, MonadReader env m, HasAwsEnv env) + :: (MonadUnliftIO m, MonadAWS m) => AwsScope -> FilePath -> m (Maybe Stack) diff --git a/src/Stackctl/Spec/Capture.hs b/src/Stackctl/Spec/Capture.hs index 0771783..596f9ef 100644 --- a/src/Stackctl/Spec/Capture.hs +++ b/src/Stackctl/Spec/Capture.hs @@ -77,11 +77,10 @@ parseCaptureOptions = runCapture :: ( MonadMask m , MonadUnliftIO m - , MonadResource m + , MonadAWS m , MonadLogger m , MonadReader env m , HasAwsScope env - , HasAwsEnv env , HasConfig env , HasDirectoryOption env ) diff --git a/src/Stackctl/Spec/Cat.hs b/src/Stackctl/Spec/Cat.hs index b438326..bf62dd3 100644 --- a/src/Stackctl/Spec/Cat.hs +++ b/src/Stackctl/Spec/Cat.hs @@ -54,8 +54,8 @@ parseCatOptions = ) runCat - :: ( MonadMask m - , MonadResource m + :: ( MonadIO m + , MonadMask m , MonadLogger m , MonadReader env m , HasLogger env diff --git a/src/Stackctl/Spec/Changes.hs b/src/Stackctl/Spec/Changes.hs index e3fc021..662f3f8 100644 --- a/src/Stackctl/Spec/Changes.hs +++ b/src/Stackctl/Spec/Changes.hs @@ -52,12 +52,11 @@ parseChangesOptions = runChanges :: ( MonadMask m , MonadUnliftIO m - , MonadResource m + , MonadAWS m , MonadLogger m , MonadReader env m , HasLogger env , HasAwsScope env - , HasAwsEnv env , HasConfig env , HasDirectoryOption env , HasFilterOption env diff --git a/src/Stackctl/Spec/Deploy.hs b/src/Stackctl/Spec/Deploy.hs index ffc632d..53094a7 100644 --- a/src/Stackctl/Spec/Deploy.hs +++ b/src/Stackctl/Spec/Deploy.hs @@ -71,12 +71,11 @@ parseDeployOptions = runDeploy :: ( MonadMask m , MonadUnliftIO m - , MonadResource m + , MonadAWS m , MonadLogger m , MonadReader env m , HasLogger env , HasAwsScope env - , HasAwsEnv env , HasConfig env , HasDirectoryOption env , HasFilterOption env @@ -114,12 +113,12 @@ runDeploy DeployOptions {..} = do when sdoClean $ awsCloudFormationDeleteAllChangeSets stackName deleteRemovedStack - :: ( MonadMask m - , MonadResource m + :: ( MonadIO m + , MonadMask m + , MonadAWS m , MonadLogger m , MonadReader env m , HasLogger env - , HasAwsEnv env ) => DeployConfirmation -> Stack @@ -146,11 +145,10 @@ data DeployConfirmation checkIfStackRequiresDeletion :: ( MonadUnliftIO m - , MonadResource m + , MonadAWS m , MonadLogger m , MonadReader env m , HasLogger env - , HasAwsEnv env ) => DeployConfirmation -> StackName @@ -174,7 +172,7 @@ checkIfStackRequiresDeletion confirmation stackName = do deleteStack stackName deleteStack - :: (MonadResource m, MonadLogger m, MonadReader env m, HasAwsEnv env) + :: (MonadIO m, MonadAWS m, MonadLogger m) => StackName -> m () deleteStack stackName = do @@ -186,11 +184,10 @@ deleteStack stackName = do deployChangeSet :: ( MonadUnliftIO m - , MonadResource m + , MonadAWS m , MonadLogger m , MonadReader env m , HasLogger env - , HasAwsEnv env ) => DeployConfirmation -> ChangeSet @@ -232,11 +229,11 @@ deployChangeSet confirmation changeSet = do changeSetId = csChangeSetId changeSet tailStackEventsSince - :: ( MonadResource m + :: ( MonadIO m + , MonadAWS m , MonadLogger m , MonadReader env m , HasLogger env - , HasAwsEnv env ) => StackName -> Maybe Text diff --git a/src/Stackctl/Spec/Discover.hs b/src/Stackctl/Spec/Discover.hs index 3e6a937..a0b6e2a 100644 --- a/src/Stackctl/Spec/Discover.hs +++ b/src/Stackctl/Spec/Discover.hs @@ -21,8 +21,8 @@ import System.FilePath (isPathSeparator) import System.FilePath.Glob forEachSpec_ - :: ( MonadMask m - , MonadResource m + :: ( MonadIO m + , MonadMask m , MonadLogger m , MonadReader env m , HasAwsScope env @@ -35,8 +35,8 @@ forEachSpec_ forEachSpec_ f = traverse_ f =<< discoverSpecs discoverSpecs - :: ( MonadMask m - , MonadResource m + :: ( MonadIO m + , MonadMask m , MonadLogger m , MonadReader env m , HasAwsScope env diff --git a/src/Stackctl/Spec/List.hs b/src/Stackctl/Spec/List.hs index f02355d..90aecac 100644 --- a/src/Stackctl/Spec/List.hs +++ b/src/Stackctl/Spec/List.hs @@ -37,11 +37,10 @@ parseListOptions = runList :: ( MonadUnliftIO m , MonadMask m - , MonadResource m + , MonadAWS m , MonadLogger m , MonadReader env m , HasAwsScope env - , HasAwsEnv env , HasLogger env , HasConfig env , HasDirectoryOption env diff --git a/src/Stackctl/StackSpec.hs b/src/Stackctl/StackSpec.hs index 77cf477..1ea2f17 100644 --- a/src/Stackctl/StackSpec.hs +++ b/src/Stackctl/StackSpec.hs @@ -185,10 +185,8 @@ readStackSpec dir specPath = do -- | Create a Change Set between a Stack Specification and deployed state createChangeSet :: ( MonadUnliftIO m - , MonadResource m , MonadLogger m - , MonadReader env m - , HasAwsEnv env + , MonadAWS m ) => StackSpec -> [Parameter] diff --git a/stack.yaml b/stack.yaml index 2511a0d..559296b 100644 --- a/stack.yaml +++ b/stack.yaml @@ -14,6 +14,7 @@ extra-deps: - amazonka-lambda-2.0 - amazonka-sso-2.0 - amazonka-sts-2.0 + - amazonka-mtl-0.1.1.0 - hspec-golden-0.2.1.0 diff --git a/stack.yaml.lock b/stack.yaml.lock index a088c0b..78e28b3 100644 --- a/stack.yaml.lock +++ b/stack.yaml.lock @@ -88,6 +88,13 @@ packages: size: 2880 original: hackage: amazonka-sts-2.0 +- completed: + hackage: amazonka-mtl-0.1.1.0@sha256:6735b3b77b38d705512480bf52e0602d35750b30b96d8a4a6dfc5025fcbe8358,6295 + pantry-tree: + sha256: e99311ec10875513e38d9402c73199bc567dddfffa6087769fe1889889627cd3 + size: 965 + original: + hackage: amazonka-mtl-0.1.1.0 - completed: hackage: hspec-golden-0.2.1.0@sha256:b695ae72685bbb5acd04cdd79d07c43de5ab8867e28662dd1a0002296f2a4940,2635 pantry-tree: diff --git a/stackctl.cabal b/stackctl.cabal index 4de63c6..00aac36 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -109,6 +109,7 @@ library , amazonka-core >=2.0 , amazonka-ec2 >=2.0 , amazonka-lambda >=2.0 + , amazonka-mtl , amazonka-sso >=2.0 , amazonka-sts >=2.0 , base ==4.* From 2966500be70b57d9013382c21eb266328a9d4cf6 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Mon, 16 Oct 2023 08:06:20 -0400 Subject: [PATCH 099/187] Version bump --- CHANGELOG.md | 8 +++++++- package.yaml | 2 +- stackctl.cabal | 4 ++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f7691d..6164caa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,10 @@ -## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.5.0.1...main) +## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.6.0.0...main) + +## [v1.6.0.0](https://github.com/freckle/stackctl/compare/v1.5.0.1...v1.6.0.0) + +- Re-implement `Stackctl.AWS` with `amazonka-mtl`. + +_No CLI or behavior changes._ ## [v1.5.0.1](https://github.com/freckle/stackctl/compare/v1.5.0.0...v1.5.0.1) diff --git a/package.yaml b/package.yaml index a0b7ffb..8cd3bf7 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: stackctl -version: 1.5.0.1 +version: 1.6.0.0 github: freckle/stackctl license: MIT author: Freckle Engineering diff --git a/stackctl.cabal b/stackctl.cabal index 00aac36..ff9fcc1 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -1,11 +1,11 @@ cabal-version: 1.18 --- This file has been generated from package.yaml by hpack version 0.35.2. +-- This file has been generated from package.yaml by hpack version 0.36.0. -- -- see: https://github.com/sol/hpack name: stackctl -version: 1.5.0.1 +version: 1.6.0.0 description: Please see homepage: https://github.com/freckle/stackctl#readme bug-reports: https://github.com/freckle/stackctl/issues From e0ca7fc3aef3b200bea852370d9967d3a51a1cd2 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 24 Oct 2023 09:19:12 -0400 Subject: [PATCH 100/187] Add basics of aws test machinery Use it to test a simple AWS-calling function. --- package.yaml | 5 ++++ stackctl.cabal | 9 +++++- test/Stackctl/AWS/EC2Spec.hs | 29 +++++++++++++++++++ test/Stackctl/Test/App.hs | 54 ++++++++++++++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 test/Stackctl/AWS/EC2Spec.hs create mode 100644 test/Stackctl/Test/App.hs diff --git a/package.yaml b/package.yaml index 8cd3bf7..32cec2a 100644 --- a/package.yaml +++ b/package.yaml @@ -116,13 +116,18 @@ tests: main: Spec.hs source-dirs: test dependencies: + - Blammo - Glob - QuickCheck - aeson + - amazonka-ec2 + - amazonka-mtl - bytestring - filepath - hspec + - hspec-expectations-lifted - hspec-golden >= 0.2.1.0 + - lens - mtl - stackctl - yaml diff --git a/stackctl.cabal b/stackctl.cabal index ff9fcc1..545f926 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -184,6 +184,7 @@ test-suite spec main-is: Spec.hs other-modules: Stackctl.AWS.CloudFormationSpec + Stackctl.AWS.EC2Spec Stackctl.AWS.ScopeSpec Stackctl.Config.RequiredVersionSpec Stackctl.ConfigSpec @@ -193,6 +194,7 @@ test-suite spec Stackctl.StackDescriptionSpec Stackctl.StackSpecSpec Stackctl.StackSpecYamlSpec + Stackctl.Test.App Paths_stackctl hs-source-dirs: test @@ -224,14 +226,19 @@ test-suite spec TypeFamilies ghc-options: -fignore-optim-changes -fwrite-ide-info -Weverything -Wno-all-missed-specialisations -Wno-missed-specialisations -Wno-missing-import-lists -Wno-missing-kind-signatures -Wno-missing-local-signatures -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module -Wno-unsafe -optP-Wno-nonportable-include-path build-depends: - Glob + Blammo + , Glob , QuickCheck , aeson + , amazonka-ec2 + , amazonka-mtl , base ==4.* , bytestring , filepath , hspec + , hspec-expectations-lifted , hspec-golden >=0.2.1.0 + , lens , mtl , stackctl , yaml diff --git a/test/Stackctl/AWS/EC2Spec.hs b/test/Stackctl/AWS/EC2Spec.hs new file mode 100644 index 0000000..df71979 --- /dev/null +++ b/test/Stackctl/AWS/EC2Spec.hs @@ -0,0 +1,29 @@ +module Stackctl.AWS.EC2Spec + ( spec + ) where + +import Stackctl.Test.App + +import Amazonka.EC2.DescribeAvailabilityZones +import Amazonka.EC2.Types.AvailabilityZone +import Stackctl.AWS.EC2 + +spec :: Spec +spec = do + describe "awsEc2DescribeFirstAvailabilityZoneRegionName" $ do + it "returns the first AZ's region name" $ example $ runTestAppT $ do + let + zones = + [ newAvailabilityZone & availabilityZone_regionName ?~ "us-east-1" + , newAvailabilityZone & availabilityZone_regionName ?~ "us-east-2" + , newAvailabilityZone & availabilityZone_regionName ?~ "us-west-1" + ] + matcher = + SendMatcher (const @_ @DescribeAvailabilityZones True) + $ Right + $ newDescribeAvailabilityZonesResponse 200 + & describeAvailabilityZonesResponse_availabilityZones + ?~ zones + + withMatcher matcher awsEc2DescribeFirstAvailabilityZoneRegionName + `shouldReturn` "us-east-1" diff --git a/test/Stackctl/Test/App.hs b/test/Stackctl/Test/App.hs new file mode 100644 index 0000000..d7b5c3b --- /dev/null +++ b/test/Stackctl/Test/App.hs @@ -0,0 +1,54 @@ +module Stackctl.Test.App + ( TestAppT + , runTestAppT + + -- * Re-exports + , module Stackctl.Prelude + , module Control.Lens + , module Control.Monad.AWS.ViaMock + , module Test.Hspec + , module Test.Hspec.Expectations.Lifted + ) where + +import Stackctl.Prelude + +import Blammo.Logging.Logger (newTestLogger) +import Control.Lens ((?~)) +import Control.Monad.AWS +import Control.Monad.AWS.ViaMock +import Test.Hspec (Spec, describe, example, it) +import Test.Hspec.Expectations.Lifted + +data TestApp = TestApp + { taLogger :: Logger + , taMatchers :: Matchers + } + +instance HasLogger TestApp where + loggerL = lens taLogger $ \x y -> x {taLogger = y} + +instance HasMatchers TestApp where + matchersL = lens taMatchers $ \x y -> x {taMatchers = y} + +newtype TestAppT m a = TestAppT + { unTestAppT :: ReaderT TestApp (LoggingT m) a + } + deriving newtype + ( Functor + , Applicative + , Monad + , MonadIO + , MonadUnliftIO + , MonadLogger + , MonadReader TestApp + ) + deriving (MonadAWS) via (MockAWS (TestAppT m)) + +runTestAppT :: MonadUnliftIO m => TestAppT m a -> m a +runTestAppT f = do + app <- + TestApp + <$> newTestLogger defaultLogSettings + <*> pure mempty + + runLoggerLoggingT app $ runReaderT (unTestAppT f) app From 2931bbde2c7f30f36ef2c080833e762244f7c207 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 24 Oct 2023 09:45:16 -0400 Subject: [PATCH 101/187] Add spec on Lambda invocations This shows using multiple matchers on the same type of request. It also motivated a useful `MonadFail` instance for pattern matches within `TestAppT`. --- package.yaml | 1 + src/Stackctl/AWS/Lambda.hs | 2 +- stackctl.cabal | 2 + test/Stackctl/AWS/LambdaSpec.hs | 65 +++++++++++++++++++++++++++++++++ test/Stackctl/Test/App.hs | 3 ++ 5 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 test/Stackctl/AWS/LambdaSpec.hs diff --git a/package.yaml b/package.yaml index 32cec2a..e8395ab 100644 --- a/package.yaml +++ b/package.yaml @@ -121,6 +121,7 @@ tests: - QuickCheck - aeson - amazonka-ec2 + - amazonka-lambda - amazonka-mtl - bytestring - filepath diff --git a/src/Stackctl/AWS/Lambda.hs b/src/Stackctl/AWS/Lambda.hs index 6df5ccc..61684ae 100644 --- a/src/Stackctl/AWS/Lambda.hs +++ b/src/Stackctl/AWS/Lambda.hs @@ -60,7 +60,7 @@ data LambdaError = LambdaError , errorMessage :: Text , trace :: [Text] } - deriving stock (Show, Generic) + deriving stock (Eq, Show, Generic) deriving anyclass (FromJSON, ToJSON) awsLambdaInvoke diff --git a/stackctl.cabal b/stackctl.cabal index 545f926..a554459 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -185,6 +185,7 @@ test-suite spec other-modules: Stackctl.AWS.CloudFormationSpec Stackctl.AWS.EC2Spec + Stackctl.AWS.LambdaSpec Stackctl.AWS.ScopeSpec Stackctl.Config.RequiredVersionSpec Stackctl.ConfigSpec @@ -231,6 +232,7 @@ test-suite spec , QuickCheck , aeson , amazonka-ec2 + , amazonka-lambda , amazonka-mtl , base ==4.* , bytestring diff --git a/test/Stackctl/AWS/LambdaSpec.hs b/test/Stackctl/AWS/LambdaSpec.hs new file mode 100644 index 0000000..bfb3b92 --- /dev/null +++ b/test/Stackctl/AWS/LambdaSpec.hs @@ -0,0 +1,65 @@ +module Stackctl.AWS.LambdaSpec + ( spec + ) where + +import Stackctl.Test.App + +import Amazonka.Lambda.Invoke +import Data.Aeson +import qualified Data.ByteString.Lazy as BSL +import Stackctl.AWS.Lambda + +spec :: Spec +spec = do + describe "awsLambdaInvoke" $ do + it "invokes a lambda" $ example $ runTestAppT $ do + let + emptyObject = object [] + + isInvocation name invoke = + and + [ invoke ^. invoke_functionName == name + , invoke ^. invoke_payload == "{}" + ] + + lambdaError = + LambdaError + { errorType = "exception" + , errorMessage = "oops" + , trace = [] + } + + matchers = + [ SendMatcher (isInvocation "lambda-1") + $ Right + $ newInvokeResponse 200 + & invokeResponse_payload + ?~ "" + , SendMatcher (isInvocation "lambda-2") + $ Right + $ newInvokeResponse 200 + & invokeResponse_payload + ?~ BSL.toStrict (encode lambdaError) + , SendMatcher (isInvocation "lambda-3") + $ Right + $ newInvokeResponse 500 + & (invokeResponse_payload ?~ "") + . (invokeResponse_functionError ?~ "") + ] + + withMatchers matchers $ do + LambdaInvokeSuccess successPayload <- + awsLambdaInvoke "lambda-1" emptyObject + + successPayload `shouldBe` "" + + LambdaInvokeError errorPayload _ <- + awsLambdaInvoke "lambda-2" emptyObject + + errorPayload `shouldBe` lambdaError + + LambdaInvokeFailure failureStatus failureFunctionError <- + awsLambdaInvoke "lambda-3" emptyObject + + failureStatus `shouldBe` 500 + failureFunctionError `shouldBe` Just "" diff --git a/test/Stackctl/Test/App.hs b/test/Stackctl/Test/App.hs index d7b5c3b..a181115 100644 --- a/test/Stackctl/Test/App.hs +++ b/test/Stackctl/Test/App.hs @@ -44,6 +44,9 @@ newtype TestAppT m a = TestAppT ) deriving (MonadAWS) via (MockAWS (TestAppT m)) +instance MonadIO m => MonadFail (TestAppT m) where + fail msg = expectationFailure msg >> error "unreachable" + runTestAppT :: MonadUnliftIO m => TestAppT m a -> m a runTestAppT f = do app <- From 1cbb4ed58a3294af6a45b98510deee0a167b7e6d Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 24 Oct 2023 10:05:44 -0400 Subject: [PATCH 102/187] Test awsCloudFormationDeleteChangeSets This exercises matching different types of requests, including paginated requests, in the same test. It also shows asserting what messages were logged. --- package.yaml | 1 + stackctl.cabal | 1 + test/Stackctl/AWS/CloudFormationSpec.hs | 80 +++++++++++++++++++++++-- 3 files changed, 76 insertions(+), 6 deletions(-) diff --git a/package.yaml b/package.yaml index e8395ab..7693d0c 100644 --- a/package.yaml +++ b/package.yaml @@ -120,6 +120,7 @@ tests: - Glob - QuickCheck - aeson + - amazonka-cloudformation - amazonka-ec2 - amazonka-lambda - amazonka-mtl diff --git a/stackctl.cabal b/stackctl.cabal index a554459..6ea426e 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -231,6 +231,7 @@ test-suite spec , Glob , QuickCheck , aeson + , amazonka-cloudformation , amazonka-ec2 , amazonka-lambda , amazonka-mtl diff --git a/test/Stackctl/AWS/CloudFormationSpec.hs b/test/Stackctl/AWS/CloudFormationSpec.hs index f99bb0f..399df53 100644 --- a/test/Stackctl/AWS/CloudFormationSpec.hs +++ b/test/Stackctl/AWS/CloudFormationSpec.hs @@ -2,27 +2,95 @@ module Stackctl.AWS.CloudFormationSpec ( spec ) where -import Stackctl.Prelude +import Stackctl.Test.App +import Amazonka.CloudFormation.DeleteChangeSet +import Amazonka.CloudFormation.ListChangeSets +import Amazonka.CloudFormation.Types.ChangeSetSummary +import Blammo.Logging.Logger (LoggedMessage (..), getLoggedMessagesUnsafe) +import qualified Data.Aeson.KeyMap as KeyMap import Data.List (isSuffixOf) import Stackctl.AWS.CloudFormation -import Test.Hspec spec :: Spec spec = do describe "readParameter" $ do - it "refuses empty key" $ do + it "refuses empty key" $ example $ do readParameter "=Value" `shouldSatisfy` either ("empty KEY" `isSuffixOf`) (const False) - it "refuses empty value" $ do + it "refuses empty value" $ example $ do readParameter "Key" `shouldSatisfy` either ("empty VALUE" `isSuffixOf`) (const False) - it "refuses empty value (with =)" $ do + it "refuses empty value (with =)" $ example $ do readParameter "Key=" `shouldSatisfy` either ("empty VALUE" `isSuffixOf`) (const False) - it "creates a parameter when valid" $ do + it "creates a parameter when valid" $ example $ do readParameter "Key=Value=More" `shouldBe` Right (makeParameter "Key" $ Just "Value=More") + + describe "awsCloudFormationDeleteAllChangeSets" $ do + it "deletes all listed changesets" $ example $ runTestAppT $ do + let + stackName :: Text + stackName = "some-stack" + + cs1 :: Text + cs1 = "some-changeset-1" + + cs2 :: Text + cs2 = "some-changeset-2" + + cs3 :: Text + cs3 = "some-changeset-3" + + isListChangeSetsPage :: Maybe Text -> ListChangeSets -> Bool + isListChangeSetsPage p req = + and + [ req ^. listChangeSets_stackName == stackName + , req ^. listChangeSets_nextToken == p + ] + + isDeleteChangeSet :: Text -> DeleteChangeSet -> Bool + isDeleteChangeSet cs req = req ^. deleteChangeSet_changeSetName == cs + + summary1 = newChangeSetSummary & changeSetSummary_changeSetId ?~ cs1 + summary2 = newChangeSetSummary & changeSetSummary_changeSetId ?~ cs2 + summary3 = newChangeSetSummary & changeSetSummary_changeSetId ?~ cs3 + + matchers = + [ SendMatcher (isListChangeSetsPage Nothing) + $ Right + $ newListChangeSetsResponse 200 + & (listChangeSetsResponse_summaries ?~ [summary1, summary2]) + & (listChangeSetsResponse_nextToken ?~ "p2") + , SendMatcher (isListChangeSetsPage $ Just "p2") + $ Right + $ newListChangeSetsResponse 200 + & (listChangeSetsResponse_summaries ?~ [summary3]) + , SendMatcher (isDeleteChangeSet cs1) + $ Right + $ newDeleteChangeSetResponse 200 + , SendMatcher (isDeleteChangeSet cs2) + $ Right + $ newDeleteChangeSetResponse 200 + , SendMatcher (isDeleteChangeSet cs3) + $ Right + $ newDeleteChangeSetResponse 200 + ] + + withMatchers matchers $ do + awsCloudFormationDeleteAllChangeSets $ StackName stackName + + messages <- + map (loggedMessageText &&& loggedMessageMeta) + <$> getLoggedMessagesUnsafe + + messages + `shouldBe` [ ("Deleting all changesets", mempty) + , ("Enqueing delete", KeyMap.fromList [("changeSetId", toJSON cs1)]) + , ("Enqueing delete", KeyMap.fromList [("changeSetId", toJSON cs2)]) + , ("Enqueing delete", KeyMap.fromList [("changeSetId", toJSON cs3)]) + ] From 60005e8d2fd8c0fe548698e2d91b5a366d4531a2 Mon Sep 17 00:00:00 2001 From: Chris Martin Date: Mon, 8 Apr 2024 10:15:59 -0600 Subject: [PATCH 103/187] Upgrade github actions, GHC 9.6 config, Ord instances, nix dev shell (#70) --- .github/workflows/ci.yml | 23 +- CHANGELOG.md | 6 +- flake.lock | 285 +++++++++++++++++++++++++ flake.nix | 65 ++++++ package.yaml | 2 +- src/Stackctl/Config/RequiredVersion.hs | 4 +- stack-lts-20.4.yaml | 22 ++ stack-lts-20.4.yaml.lock | 117 ++++++++++ stack.yaml | 20 +- stack.yaml.lock | 104 +-------- stackctl.cabal | 2 +- 11 files changed, 529 insertions(+), 121 deletions(-) create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 stack-lts-20.4.yaml create mode 100644 stack-lts-20.4.yaml.lock diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b86d74c..6851f4d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,17 +10,32 @@ concurrency: cancel-in-progress: true jobs: + generate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - id: generate + uses: freckle/stack-action/generate-matrix@v5 + outputs: + stack-yamls: ${{ steps.generate.outputs.stack-yamls }} + build: + needs: generate + strategy: + matrix: + stack-yaml: ${{ fromJSON(needs.generate.outputs.stack-yamls) }} + fail-fast: false runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: freckle/stack-cache-action@v2 - - uses: freckle/stack-action@v3 + - uses: actions/checkout@v4 + - uses: freckle/stack-action@v5 + with: + stack-arguments: --stack-yaml ${{ matrix.stack-yaml }} lint: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: haskell/actions/hlint-setup@v2 - uses: haskell/actions/hlint-run@v2 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index 6164caa..f934677 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ -## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.6.0.0...main) +## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.6.1.0...main) + +## [v1.6.1.0](https://github.com/freckle/stackctl/compare/v1.6.0.0...v1.6.1.0) + +- Add `Ord` instance on `RequiredVersion` and `RequiredVersionOp` ## [v1.6.0.0](https://github.com/freckle/stackctl/compare/v1.5.0.1...v1.6.0.0) diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..78b3c65 --- /dev/null +++ b/flake.lock @@ -0,0 +1,285 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1705309234, + "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "freckle": { + "inputs": { + "flake-utils": "flake-utils_2", + "nixpkgs-22-11": "nixpkgs-22-11", + "nixpkgs-23-05": "nixpkgs-23-05", + "nixpkgs-master-2023-05-06": "nixpkgs-master-2023-05-06", + "nixpkgs-master-2023-07-18": "nixpkgs-master-2023-07-18", + "nixpkgs-master-2023-09-15": "nixpkgs-master-2023-09-15", + "nixpkgs-master-2024-01-27": "nixpkgs-master-2024-01-27", + "nixpkgs-stable": "nixpkgs-stable", + "nixpkgs-stable-2023-07-25": "nixpkgs-stable-2023-07-25", + "nixpkgs-unstable-2023-10-21": "nixpkgs-unstable-2023-10-21", + "nixpkgs-unstable-2024-02-20": "nixpkgs-unstable-2024-02-20" + }, + "locked": { + "dir": "main", + "lastModified": 1708474311, + "narHash": "sha256-nO5JLvAshKODkumut9gnMrb9Uqh9PPNnWfXPM3P/kRw=", + "ref": "refs/heads/main", + "rev": "ace145f01993ddc109d86a4c47e37ffe06481df3", + "revCount": 29, + "type": "git", + "url": "ssh://git@github.com/freckle/flakes?dir=main" + }, + "original": { + "dir": "main", + "type": "git", + "url": "ssh://git@github.com/freckle/flakes?dir=main" + } + }, + "nixpkgs-22-11": { + "locked": { + "lastModified": 1688392541, + "narHash": "sha256-lHrKvEkCPTUO+7tPfjIcb7Trk6k31rz18vkyqmkeJfY=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "ea4c80b39be4c09702b0cb3b42eab59e2ba4f24b", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-22.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-23-05": { + "locked": { + "lastModified": 1704290814, + "narHash": "sha256-LWvKHp7kGxk/GEtlrGYV68qIvPHkU9iToomNFGagixU=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "70bdadeb94ffc8806c0570eb5c2695ad29f0e421", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-23.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-master-2023-05-06": { + "locked": { + "lastModified": 1683392273, + "narHash": "sha256-pZTuxvcuDeBG+vvE1zczNyEUzlPbzXVh8Ed45Fzo+tQ=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "16b3b0c53b1ee8936739f8c588544e7fcec3fc60", + "type": "github" + }, + "original": { + "owner": "nixos", + "repo": "nixpkgs", + "rev": "16b3b0c53b1ee8936739f8c588544e7fcec3fc60", + "type": "github" + } + }, + "nixpkgs-master-2023-07-18": { + "locked": { + "lastModified": 1689680872, + "narHash": "sha256-brNix2+ihJSzCiKwLafbyejrHJZUP0Fy6z5+xMOC27M=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "08700de174bc6235043cb4263b643b721d936bdb", + "type": "github" + }, + "original": { + "owner": "nixos", + "repo": "nixpkgs", + "rev": "08700de174bc6235043cb4263b643b721d936bdb", + "type": "github" + } + }, + "nixpkgs-master-2023-09-15": { + "locked": { + "lastModified": 1694760568, + "narHash": "sha256-3G07BiXrp2YQKxdcdms22MUx6spc6A++MSePtatCYuI=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "46688f8eb5cd6f1298d873d4d2b9cf245e09e88e", + "type": "github" + }, + "original": { + "owner": "nixos", + "repo": "nixpkgs", + "rev": "46688f8eb5cd6f1298d873d4d2b9cf245e09e88e", + "type": "github" + } + }, + "nixpkgs-master-2024-01-27": { + "locked": { + "lastModified": 1706367331, + "narHash": "sha256-AqgkGHRrI6h/8FWuVbnkfFmXr4Bqsr4fV23aISqj/xg=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "160b762eda6d139ac10ae081f8f78d640dd523eb", + "type": "github" + }, + "original": { + "owner": "nixos", + "repo": "nixpkgs", + "rev": "160b762eda6d139ac10ae081f8f78d640dd523eb", + "type": "github" + } + }, + "nixpkgs-stable": { + "locked": { + "lastModified": 1708294118, + "narHash": "sha256-evZzmLW7qoHXf76VCepvun1esZDxHfVRFUJtumD7L2M=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "e0da498ad77ac8909a980f07eff060862417ccf7", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-23.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-stable-2023-07-25": { + "locked": { + "lastModified": 1690271650, + "narHash": "sha256-qwdsW8DBY1qH+9luliIH7VzgwvL+ZGI3LZWC0LTiDMI=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "6dc93f0daec55ee2f441da385aaf143863e3d671", + "type": "github" + }, + "original": { + "owner": "nixos", + "repo": "nixpkgs", + "rev": "6dc93f0daec55ee2f441da385aaf143863e3d671", + "type": "github" + } + }, + "nixpkgs-unstable-2023-10-21": { + "locked": { + "lastModified": 1697793076, + "narHash": "sha256-02e7sCuqLtkyRgrZmdOyvAcQTQdcXj+vpyp9bca6cY4=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "038b2922be3fc096e1d456f93f7d0f4090628729", + "type": "github" + }, + "original": { + "owner": "nixos", + "repo": "nixpkgs", + "rev": "038b2922be3fc096e1d456f93f7d0f4090628729", + "type": "github" + } + }, + "nixpkgs-unstable-2024-02-20": { + "locked": { + "lastModified": 1708296515, + "narHash": "sha256-FyF489fYNAUy7b6dkYV6rGPyzp+4tThhr80KNAaF/yY=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "b98a4e1746acceb92c509bc496ef3d0e5ad8d4aa", + "type": "github" + }, + "original": { + "owner": "nixos", + "repo": "nixpkgs", + "rev": "b98a4e1746acceb92c509bc496ef3d0e5ad8d4aa", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "freckle": "freckle", + "stable": "stable" + } + }, + "stable": { + "locked": { + "lastModified": 1712168706, + "narHash": "sha256-XP24tOobf6GGElMd0ux90FEBalUtw6NkBSVh/RlA6ik=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "1487bdea619e4a7a53a4590c475deabb5a9d1bfb", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-23.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..74312be --- /dev/null +++ b/flake.nix @@ -0,0 +1,65 @@ +{ + inputs = { + stable.url = "github:nixos/nixpkgs/nixos-23.11"; + freckle.url = "git+ssh://git@github.com/freckle/flakes?dir=main"; + flake-utils.url = "github:numtide/flake-utils"; + }; + outputs = inputs: inputs.flake-utils.lib.eachDefaultSystem (system: + let + nixpkgsArgs = { inherit system; config = { }; }; + nixpkgs = { + stable = import inputs.stable nixpkgsArgs; + }; + freckle = inputs.freckle.packages.${system}; + freckleLib = inputs.freckle.lib.${system}; + in + rec { + packages = { + awscli = freckle.aws-cli-2-11-x; + + cabal = nixpkgs.stable.cabal-install; + + fourmolu = freckle.fourmolu-0-13-x; + + ghc = freckleLib.haskellBundle { + ghcVersion = "ghc-9-6-3"; + packageSelection = p: [ ]; + enableHLS = true; + }; + + hlint = + nixpkgs.stable.haskell.lib.justStaticExecutables + nixpkgs.stable.hlint; + + stack = nixpkgs.stable.writeShellApplication { + name = "stack"; + text = '' + ${nixpkgs.stable.stack}/bin/stack --system-ghc --no-nix "$@" + ''; + } + ; + }; + + devShells.default = nixpkgs.stable.mkShell { + buildInputs = with (nixpkgs.stable); [ + pcre + pcre.dev + zlib + zlib.dev + ]; + + nativeBuildInputs = with (packages); [ + awscli + cabal + fourmolu + ghc + hlint + stack + ]; + + shellHook = '' + export STACK_YAML=stack.yaml + ''; + }; + }); +} diff --git a/package.yaml b/package.yaml index 7693d0c..80b1e5a 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: stackctl -version: 1.6.0.0 +version: 1.6.1.0 github: freckle/stackctl license: MIT author: Freckle Engineering diff --git a/src/Stackctl/Config/RequiredVersion.hs b/src/Stackctl/Config/RequiredVersion.hs index 0d83a22..56e119e 100644 --- a/src/Stackctl/Config/RequiredVersion.hs +++ b/src/Stackctl/Config/RequiredVersion.hs @@ -24,7 +24,7 @@ data RequiredVersion = RequiredVersion { requiredVersionOp :: RequiredVersionOp , requiredVersionCompareWith :: Version } - deriving stock (Eq, Show) + deriving stock (Eq, Ord, Show) instance Arbitrary RequiredVersion where arbitrary = RequiredVersion <$> arbitrary <*> arbitrary @@ -96,7 +96,7 @@ data RequiredVersionOp | RequiredVersionGT | RequiredVersionGTE | RequiredVersionIsh - deriving stock (Eq, Show, Bounded, Enum) + deriving stock (Eq, Ord, Show, Bounded, Enum) instance Arbitrary RequiredVersionOp where arbitrary = arbitraryBoundedEnum diff --git a/stack-lts-20.4.yaml b/stack-lts-20.4.yaml new file mode 100644 index 0000000..559296b --- /dev/null +++ b/stack-lts-20.4.yaml @@ -0,0 +1,22 @@ +resolver: lts-20.4 + +extra-deps: + - Blammo-1.1.2.1 + - cfn-flip-0.1.0.3 + - unliftio-0.2.25.0 + + - amazonka-2.0 + - amazonka-core-2.0 + - amazonka-certificatemanager-2.0 + - amazonka-cloudformation-2.0 + - amazonka-ec2-2.0 + - amazonka-ecr-2.0 + - amazonka-lambda-2.0 + - amazonka-sso-2.0 + - amazonka-sts-2.0 + - amazonka-mtl-0.1.1.0 + + - hspec-golden-0.2.1.0 + + # For amazonka-core-2.0 + - crypton-0.33 diff --git a/stack-lts-20.4.yaml.lock b/stack-lts-20.4.yaml.lock new file mode 100644 index 0000000..78e28b3 --- /dev/null +++ b/stack-lts-20.4.yaml.lock @@ -0,0 +1,117 @@ +# This file was autogenerated by Stack. +# You should not edit this file by hand. +# For more information, please see the documentation at: +# https://docs.haskellstack.org/en/stable/lock_files + +packages: +- completed: + hackage: Blammo-1.1.2.1@sha256:b74d553fb3557bb10381b806bd34b8bad0b800883f02dfd1cc847f58db40958c,4084 + pantry-tree: + sha256: bd28931f07beaaae8565a87d8c3b55d3e9ff5c332ae93dc32c1090a4c814e620 + size: 1567 + original: + hackage: Blammo-1.1.2.1 +- completed: + hackage: cfn-flip-0.1.0.3@sha256:8737882d818d74b29d3b1791a4df4dc89995870312374989c47c29352ea503ec,5615 + pantry-tree: + sha256: 715102dfcca7053390eda5be0504485fb93b8b84226fe373a6e62d297090d49b + size: 3139 + original: + hackage: cfn-flip-0.1.0.3 +- completed: + hackage: unliftio-0.2.25.0@sha256:d015242554890370bcbc3a575019be691d0edc279736ef97d29412fb9d0c4349,3410 + pantry-tree: + sha256: 08c62f256e740e1a78b175907c26cb06439a1b486ceb8021c5a2e4425ebb6c5b + size: 2494 + original: + hackage: unliftio-0.2.25.0 +- completed: + hackage: amazonka-2.0@sha256:3481da2fda6b210d15d41c1db7a588adf68123cfb7ea3882797a6230003259db,3505 + pantry-tree: + sha256: 01c7121bd5e4a3918a71ea6502412292c97facf20c9620f07af96e423d6437e2 + size: 1528 + original: + hackage: amazonka-2.0 +- completed: + hackage: amazonka-core-2.0@sha256:d9f0533c272ac92bd7b18699077038b6b51b3552e91b65743af4ce646286b4f8,4383 + pantry-tree: + sha256: 46e7e4de910b08ee2df98db9cda2becf388ce49510024018289a46c43e175ee0 + size: 3222 + original: + hackage: amazonka-core-2.0 +- completed: + hackage: amazonka-certificatemanager-2.0@sha256:9a203a46ec1eaae2c59aa891efa480f84411783d02ba973820d67e95cc67756c,5226 + pantry-tree: + sha256: 9ee7f26c6166f2b01f32efcf41d4a6315ff681823c698f49036f5b471ffb6e9c + size: 7191 + original: + hackage: amazonka-certificatemanager-2.0 +- completed: + hackage: amazonka-cloudformation-2.0@sha256:7a9618bf697cdaf0a51c2d7be557ad47820b926416d79f5138ff3befdbfcbafb,11870 + pantry-tree: + sha256: 177fbc16ea2fa072a7fca9f4a3b1d64f4d5e8fc7cd493e4e841f337c572745bd + size: 27257 + original: + hackage: amazonka-cloudformation-2.0 +- completed: + hackage: amazonka-ec2-2.0@sha256:9344b87d8f8328fd91023b96565e79e7676aa5e7dd40b87b3f3f3a22a9da7736,74154 + pantry-tree: + sha256: d1f2d4fce5b0664605d730d4232b25f26a5f49e3b7d07f4b282e8c36773e5ffd + size: 234434 + original: + hackage: amazonka-ec2-2.0 +- completed: + hackage: amazonka-ecr-2.0@sha256:88ec5dffb3c07f9e49eb4d9672ac62c175b6cf2c3e044ec0e4c705cd6bff3487,6925 + pantry-tree: + sha256: d0d5dc0ed4aab28f0d6183657e77985693c5ed011dd0cc40d335b6a334b1939a + size: 15627 + original: + hackage: amazonka-ecr-2.0 +- completed: + hackage: amazonka-lambda-2.0@sha256:aa74299380318b04429980eb76b7f0499a8241ff01de859042b0ff09bd7ef420,8281 + pantry-tree: + sha256: da8f346de9d1eb0fb12afa91e44f8179ac05176043919346e2e72a7880b7a9e5 + size: 21343 + original: + hackage: amazonka-lambda-2.0 +- completed: + hackage: amazonka-sso-2.0@sha256:902be13b604e4a3b51a9b8e1adc6a32f42322ae11f738a72a8c737b2d0a91a5e,2995 + pantry-tree: + sha256: f87dd959a78bf54295bd6f8c7da58f7f8f860251d5548ecb05ab758e03cba50b + size: 1817 + original: + hackage: amazonka-sso-2.0 +- completed: + hackage: amazonka-sts-2.0@sha256:5c721083e8d80883a893176de6105c27bbbd8176f467c27ac5f8d548a5e726d8,3209 + pantry-tree: + sha256: bde4691af7cac74e0a3705271b4d3ac05515863bfb6f668112e3f3950a27cb41 + size: 2880 + original: + hackage: amazonka-sts-2.0 +- completed: + hackage: amazonka-mtl-0.1.1.0@sha256:6735b3b77b38d705512480bf52e0602d35750b30b96d8a4a6dfc5025fcbe8358,6295 + pantry-tree: + sha256: e99311ec10875513e38d9402c73199bc567dddfffa6087769fe1889889627cd3 + size: 965 + original: + hackage: amazonka-mtl-0.1.1.0 +- completed: + hackage: hspec-golden-0.2.1.0@sha256:b695ae72685bbb5acd04cdd79d07c43de5ab8867e28662dd1a0002296f2a4940,2635 + pantry-tree: + sha256: d72fec5f2c0568ae958282c7a8b8f5bfba146e3e4ceee0510c0e22be5c8eb740 + size: 495 + original: + hackage: hspec-golden-0.2.1.0 +- completed: + hackage: crypton-0.33@sha256:5e92f29b9b7104d91fcdda1dec9400c9ad1f1791c231cc41ceebd783fb517dee,18202 + pantry-tree: + sha256: 38809499d7f9775ef45cd29ab5c3dc9b283a813f34c1cdc56681b24f8cf8bb4f + size: 23148 + original: + hackage: crypton-0.33 +snapshots: +- completed: + sha256: 3770dfd79f5aed67acdcc65c4e7730adddffe6dba79ea723cfb0918356fc0f94 + size: 648660 + url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/20/4.yaml + original: lts-20.4 diff --git a/stack.yaml b/stack.yaml index 559296b..28029a9 100644 --- a/stack.yaml +++ b/stack.yaml @@ -1,22 +1,6 @@ -resolver: lts-20.4 +resolver: lts-22.6 extra-deps: - Blammo-1.1.2.1 - - cfn-flip-0.1.0.3 - - unliftio-0.2.25.0 - - - amazonka-2.0 - - amazonka-core-2.0 - - amazonka-certificatemanager-2.0 - - amazonka-cloudformation-2.0 - - amazonka-ec2-2.0 - - amazonka-ecr-2.0 - - amazonka-lambda-2.0 - - amazonka-sso-2.0 - - amazonka-sts-2.0 - amazonka-mtl-0.1.1.0 - - - hspec-golden-0.2.1.0 - - # For amazonka-core-2.0 - - crypton-0.33 + - cfn-flip-0.1.0.3 diff --git a/stack.yaml.lock b/stack.yaml.lock index 78e28b3..e296fd2 100644 --- a/stack.yaml.lock +++ b/stack.yaml.lock @@ -12,106 +12,22 @@ packages: original: hackage: Blammo-1.1.2.1 - completed: - hackage: cfn-flip-0.1.0.3@sha256:8737882d818d74b29d3b1791a4df4dc89995870312374989c47c29352ea503ec,5615 + hackage: amazonka-mtl-0.1.1.0@sha256:90b45a950c0e398b0e48d1447766f331c2ac3d5a72e15be2bf0be3b3c56159c3,6572 pantry-tree: - sha256: 715102dfcca7053390eda5be0504485fb93b8b84226fe373a6e62d297090d49b - size: 3139 - original: - hackage: cfn-flip-0.1.0.3 -- completed: - hackage: unliftio-0.2.25.0@sha256:d015242554890370bcbc3a575019be691d0edc279736ef97d29412fb9d0c4349,3410 - pantry-tree: - sha256: 08c62f256e740e1a78b175907c26cb06439a1b486ceb8021c5a2e4425ebb6c5b - size: 2494 - original: - hackage: unliftio-0.2.25.0 -- completed: - hackage: amazonka-2.0@sha256:3481da2fda6b210d15d41c1db7a588adf68123cfb7ea3882797a6230003259db,3505 - pantry-tree: - sha256: 01c7121bd5e4a3918a71ea6502412292c97facf20c9620f07af96e423d6437e2 - size: 1528 - original: - hackage: amazonka-2.0 -- completed: - hackage: amazonka-core-2.0@sha256:d9f0533c272ac92bd7b18699077038b6b51b3552e91b65743af4ce646286b4f8,4383 - pantry-tree: - sha256: 46e7e4de910b08ee2df98db9cda2becf388ce49510024018289a46c43e175ee0 - size: 3222 - original: - hackage: amazonka-core-2.0 -- completed: - hackage: amazonka-certificatemanager-2.0@sha256:9a203a46ec1eaae2c59aa891efa480f84411783d02ba973820d67e95cc67756c,5226 - pantry-tree: - sha256: 9ee7f26c6166f2b01f32efcf41d4a6315ff681823c698f49036f5b471ffb6e9c - size: 7191 - original: - hackage: amazonka-certificatemanager-2.0 -- completed: - hackage: amazonka-cloudformation-2.0@sha256:7a9618bf697cdaf0a51c2d7be557ad47820b926416d79f5138ff3befdbfcbafb,11870 - pantry-tree: - sha256: 177fbc16ea2fa072a7fca9f4a3b1d64f4d5e8fc7cd493e4e841f337c572745bd - size: 27257 - original: - hackage: amazonka-cloudformation-2.0 -- completed: - hackage: amazonka-ec2-2.0@sha256:9344b87d8f8328fd91023b96565e79e7676aa5e7dd40b87b3f3f3a22a9da7736,74154 - pantry-tree: - sha256: d1f2d4fce5b0664605d730d4232b25f26a5f49e3b7d07f4b282e8c36773e5ffd - size: 234434 - original: - hackage: amazonka-ec2-2.0 -- completed: - hackage: amazonka-ecr-2.0@sha256:88ec5dffb3c07f9e49eb4d9672ac62c175b6cf2c3e044ec0e4c705cd6bff3487,6925 - pantry-tree: - sha256: d0d5dc0ed4aab28f0d6183657e77985693c5ed011dd0cc40d335b6a334b1939a - size: 15627 - original: - hackage: amazonka-ecr-2.0 -- completed: - hackage: amazonka-lambda-2.0@sha256:aa74299380318b04429980eb76b7f0499a8241ff01de859042b0ff09bd7ef420,8281 - pantry-tree: - sha256: da8f346de9d1eb0fb12afa91e44f8179ac05176043919346e2e72a7880b7a9e5 - size: 21343 - original: - hackage: amazonka-lambda-2.0 -- completed: - hackage: amazonka-sso-2.0@sha256:902be13b604e4a3b51a9b8e1adc6a32f42322ae11f738a72a8c737b2d0a91a5e,2995 - pantry-tree: - sha256: f87dd959a78bf54295bd6f8c7da58f7f8f860251d5548ecb05ab758e03cba50b - size: 1817 - original: - hackage: amazonka-sso-2.0 -- completed: - hackage: amazonka-sts-2.0@sha256:5c721083e8d80883a893176de6105c27bbbd8176f467c27ac5f8d548a5e726d8,3209 - pantry-tree: - sha256: bde4691af7cac74e0a3705271b4d3ac05515863bfb6f668112e3f3950a27cb41 - size: 2880 - original: - hackage: amazonka-sts-2.0 -- completed: - hackage: amazonka-mtl-0.1.1.0@sha256:6735b3b77b38d705512480bf52e0602d35750b30b96d8a4a6dfc5025fcbe8358,6295 - pantry-tree: - sha256: e99311ec10875513e38d9402c73199bc567dddfffa6087769fe1889889627cd3 + sha256: c85849d4d5caa36a3597323185d7593cb624cadee6e4f05219d3ccd498a7b270 size: 965 original: hackage: amazonka-mtl-0.1.1.0 - completed: - hackage: hspec-golden-0.2.1.0@sha256:b695ae72685bbb5acd04cdd79d07c43de5ab8867e28662dd1a0002296f2a4940,2635 + hackage: cfn-flip-0.1.0.3@sha256:40f33714827c35a9fd3cebde06002f54448c3efa34252efbe5e48445065f2620,5934 pantry-tree: - sha256: d72fec5f2c0568ae958282c7a8b8f5bfba146e3e4ceee0510c0e22be5c8eb740 - size: 495 - original: - hackage: hspec-golden-0.2.1.0 -- completed: - hackage: crypton-0.33@sha256:5e92f29b9b7104d91fcdda1dec9400c9ad1f1791c231cc41ceebd783fb517dee,18202 - pantry-tree: - sha256: 38809499d7f9775ef45cd29ab5c3dc9b283a813f34c1cdc56681b24f8cf8bb4f - size: 23148 + sha256: 4d5fc2c97d269deb4a34432db02a725850392542d1852a11f10c76832611e2c8 + size: 3139 original: - hackage: crypton-0.33 + hackage: cfn-flip-0.1.0.3 snapshots: - completed: - sha256: 3770dfd79f5aed67acdcc65c4e7730adddffe6dba79ea723cfb0918356fc0f94 - size: 648660 - url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/20/4.yaml - original: lts-20.4 + sha256: 1b4c2669e26fa828451830ed4725e4d406acc25a1fa24fcc039465dd13d7a575 + size: 714100 + url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/22/6.yaml + original: lts-22.6 diff --git a/stackctl.cabal b/stackctl.cabal index 6ea426e..f534b72 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -5,7 +5,7 @@ cabal-version: 1.18 -- see: https://github.com/sol/hpack name: stackctl -version: 1.6.0.0 +version: 1.6.1.0 description: Please see homepage: https://github.com/freckle/stackctl#readme bug-reports: https://github.com/freckle/stackctl/issues From 44ad367ac11f15c102750d61af752cbb47f6e3fe Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Fri, 5 Apr 2024 11:06:14 -0400 Subject: [PATCH 104/187] Add spec on RemovedStacks --- fourmolu.yaml | 7 ++- package.yaml | 4 ++ stackctl.cabal | 5 ++ test/Stackctl/RemovedStackSpec.hs | 80 +++++++++++++++++++++++++++++++ test/Stackctl/Test/App.hs | 41 +++++++++++++++- 5 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 test/Stackctl/RemovedStackSpec.hs diff --git a/fourmolu.yaml b/fourmolu.yaml index ef571e8..9211e93 100644 --- a/fourmolu.yaml +++ b/fourmolu.yaml @@ -12,4 +12,9 @@ in-style: left-align single-constraint-parens: never # ignored until v12 / ghc-9.6 unicode: never # default respectful: true # default -fixities: [] # default + +# fourmolu can't figure this out because of the re-exports we use +fixities: + - "infixl 1 &" + - "infixr 4 .~" + - "infixr 4 ?~" diff --git a/package.yaml b/package.yaml index 80b1e5a..c4bf150 100644 --- a/package.yaml +++ b/package.yaml @@ -120,6 +120,7 @@ tests: - Glob - QuickCheck - aeson + - amazonka - amazonka-cloudformation - amazonka-ec2 - amazonka-lambda @@ -129,7 +130,10 @@ tests: - hspec - hspec-expectations-lifted - hspec-golden >= 0.2.1.0 + - http-types - lens - mtl - stackctl + - text + - time - yaml diff --git a/stackctl.cabal b/stackctl.cabal index f534b72..b3710b1 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -191,6 +191,7 @@ test-suite spec Stackctl.ConfigSpec Stackctl.FilterOptionSpec Stackctl.OneOrListOfSpec + Stackctl.RemovedStackSpec Stackctl.Spec.Changes.FormatSpec Stackctl.StackDescriptionSpec Stackctl.StackSpecSpec @@ -231,6 +232,7 @@ test-suite spec , Glob , QuickCheck , aeson + , amazonka , amazonka-cloudformation , amazonka-ec2 , amazonka-lambda @@ -241,8 +243,11 @@ test-suite spec , hspec , hspec-expectations-lifted , hspec-golden >=0.2.1.0 + , http-types , lens , mtl , stackctl + , text + , time , yaml default-language: Haskell2010 diff --git a/test/Stackctl/RemovedStackSpec.hs b/test/Stackctl/RemovedStackSpec.hs new file mode 100644 index 0000000..045d444 --- /dev/null +++ b/test/Stackctl/RemovedStackSpec.hs @@ -0,0 +1,80 @@ +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} + +module Stackctl.RemovedStackSpec + ( spec + ) where + +import Stackctl.Test.App + +import qualified Amazonka +import qualified Amazonka.CloudFormation as CloudFormation +import Amazonka.CloudFormation.DescribeStacks +import Amazonka.CloudFormation.Types.Stack +import qualified Data.Text as T +import Data.Time (UTCTime (..)) +import Data.Time.Calendar (DayOfMonth, MonthOfYear, Year, fromGregorian) +import Network.HTTP.Types.Status (status400) +import Stackctl.AWS.CloudFormation +import Stackctl.FilterOption (filterOptionFromText, filterOptionL) +import Stackctl.RemovedStack + +spec :: Spec +spec = do + describe "inferRemovedStacks" $ do + it "returns stacks in filters that aren't on disk" $ example $ runTestAppT $ do + let + Just filterOption = + filterOptionFromText + $ T.intercalate + "," + [ pack $ testAppStackFilePath "stack-exists" + , pack $ testAppStackFilePath "stack-is-missing" + , "stacks/0123456789.test/us-east-2/wrong-region.yaml" + , "stacks/2123456789.test/us-east-1/wrong-account.yaml" + ] + + setup :: TestApp -> TestApp + setup = filterOptionL .~ filterOption + + matchers = + [ describeStackMatcher "stack-exists" $ Just $ someStack "stack-exists" + , describeStackMatcher "stack-is-missing" Nothing + , describeStackMatcher "wrong-region" Nothing + , describeStackMatcher "wrong-account" Nothing + ] + + stacks <- local setup $ withMatchers matchers inferRemovedStacks + map (^. stack_stackName) stacks `shouldBe` ["stack-exists"] + +describeStackMatcher :: Text -> Maybe Stack -> Matcher +describeStackMatcher name = + SendMatcher ((== Just name) . (^. describeStacks_stackName)) + . maybe + (Left cloudFormationValidationError) + ( \stack -> + Right + $ newDescribeStacksResponse 200 + & describeStacksResponse_stacks ?~ [stack] + ) + +someStack :: Text -> Stack +someStack name = newStack name (midnight 2024 1 1) StackStatus_CREATE_COMPLETE + +midnight :: Year -> MonthOfYear -> DayOfMonth -> UTCTime +midnight y m d = + UTCTime + { utctDay = fromGregorian y m d + , utctDayTime = 0 + } + +cloudFormationValidationError :: Amazonka.Error +cloudFormationValidationError = + Amazonka.ServiceError + $ Amazonka.ServiceError' + { Amazonka.abbrev = CloudFormation.defaultService ^. Amazonka.service_abbrev + , Amazonka.status = status400 + , Amazonka.headers = [] + , Amazonka.code = "ValidationError" + , Amazonka.message = Nothing + , Amazonka.requestId = Nothing + } diff --git a/test/Stackctl/Test/App.hs b/test/Stackctl/Test/App.hs index a181115..afa45c9 100644 --- a/test/Stackctl/Test/App.hs +++ b/test/Stackctl/Test/App.hs @@ -1,5 +1,8 @@ module Stackctl.Test.App - ( TestAppT + ( TestApp + , testAppAwsScope + , testAppStackFilePath + , TestAppT , runTestAppT -- * Re-exports @@ -16,12 +19,19 @@ import Blammo.Logging.Logger (newTestLogger) import Control.Lens ((?~)) import Control.Monad.AWS import Control.Monad.AWS.ViaMock +import Stackctl.AWS.Core (AccountId (..)) +import Stackctl.AWS.Scope +import Stackctl.DirectoryOption +import Stackctl.FilterOption import Test.Hspec (Spec, describe, example, it) import Test.Hspec.Expectations.Lifted data TestApp = TestApp { taLogger :: Logger , taMatchers :: Matchers + , taAwsScope :: AwsScope + , taFilterOption :: FilterOption + , taDirectoryOption :: DirectoryOption } instance HasLogger TestApp where @@ -30,6 +40,15 @@ instance HasLogger TestApp where instance HasMatchers TestApp where matchersL = lens taMatchers $ \x y -> x {taMatchers = y} +instance HasAwsScope TestApp where + awsScopeL = lens taAwsScope $ \x y -> x {taAwsScope = y} + +instance HasFilterOption TestApp where + filterOptionL = lens taFilterOption $ \x y -> x {taFilterOption = y} + +instance HasDirectoryOption TestApp where + directoryOptionL = lens taDirectoryOption $ \x y -> x {taDirectoryOption = y} + newtype TestAppT m a = TestAppT { unTestAppT :: ReaderT TestApp (LoggingT m) a } @@ -53,5 +72,25 @@ runTestAppT f = do TestApp <$> newTestLogger defaultLogSettings <*> pure mempty + <*> pure testAppAwsScope + <*> pure defaultFilterOption + <*> pure defaultDirectoryOption runLoggerLoggingT app $ runReaderT (unTestAppT f) app + +testAppAwsScope :: AwsScope +testAppAwsScope = + AwsScope + { awsAccountId = AccountId "0123456789" + , awsAccountName = "test" + , awsRegion = "us-east-1" + } + +-- | Gives a filepath relative to 'testAwsScope' +testAppStackFilePath :: Text -> FilePath +testAppStackFilePath base = + "stacks" + "0123456789.test" + "us-east-1" + unpack base + <.> "yaml" From 5d45a30b7e3f5ec893974f3a6c6650fddcc461cb Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Fri, 5 Apr 2024 11:19:36 -0400 Subject: [PATCH 105/187] Reproduce RemovedStack bug in test --- package.yaml | 1 + stackctl.cabal | 1 + test/Stackctl/RemovedStackSpec.hs | 37 +++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/package.yaml b/package.yaml index c4bf150..ff0b643 100644 --- a/package.yaml +++ b/package.yaml @@ -136,4 +136,5 @@ tests: - stackctl - text - time + - unliftio - yaml diff --git a/stackctl.cabal b/stackctl.cabal index b3710b1..abd64b6 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -249,5 +249,6 @@ test-suite spec , stackctl , text , time + , unliftio , yaml default-language: Haskell2010 diff --git a/test/Stackctl/RemovedStackSpec.hs b/test/Stackctl/RemovedStackSpec.hs index 045d444..6137c91 100644 --- a/test/Stackctl/RemovedStackSpec.hs +++ b/test/Stackctl/RemovedStackSpec.hs @@ -15,8 +15,10 @@ import Data.Time (UTCTime (..)) import Data.Time.Calendar (DayOfMonth, MonthOfYear, Year, fromGregorian) import Network.HTTP.Types.Status (status400) import Stackctl.AWS.CloudFormation +import Stackctl.DirectoryOption (DirectoryOption (..), directoryOptionL) import Stackctl.FilterOption (filterOptionFromText, filterOptionL) import Stackctl.RemovedStack +import UnliftIO.Directory (createDirectoryIfMissing) spec :: Spec spec = do @@ -46,6 +48,41 @@ spec = do stacks <- local setup $ withMatchers matchers inferRemovedStacks map (^. stack_stackName) stacks `shouldBe` ["stack-exists"] + -- If we don't check for file existence respecting STACKCTL_DIRECTORY, then + -- any non-default value will cause all specs to appear non-existent and be + -- flagged for removal. Eek. + it "respects STACKCTL_DIRECTORY" $ example $ runTestAppT $ do + let + dir = "/tmp/stackctl-test" + toRemove = "stack-to-remove" + toKeep = "stack-to-keep" + relativeToRemove = testAppStackFilePath toRemove + relativeToKeep = testAppStackFilePath toKeep + absoluteToKeep = dir relativeToKeep + Just filterOption = + filterOptionFromText + $ pack relativeToRemove + <> "," + <> pack relativeToKeep + + setup :: TestApp -> TestApp + setup app = + app + & filterOptionL .~ filterOption + & directoryOptionL .~ DirectoryOption dir + + matchers = + [ describeStackMatcher toRemove $ Just $ someStack toRemove + , describeStackMatcher toKeep $ Just $ someStack toKeep + ] + + -- Create a spec on disk for toKeep, then we should only find toRemove + createDirectoryIfMissing True $ takeDirectory absoluteToKeep + writeFileUtf8 absoluteToKeep "{}" + + stacks <- local setup $ withMatchers matchers inferRemovedStacks + map (^. stack_stackName) stacks `shouldBe` [toRemove] + describeStackMatcher :: Text -> Maybe Stack -> Matcher describeStackMatcher name = SendMatcher ((== Just name) . (^. describeStacks_stackName)) From 98fb2a292b18d3f54f5de96c1b85a8ca8bfb6494 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Fri, 5 Apr 2024 09:55:57 -0400 Subject: [PATCH 106/187] Respect STACKCTL_DIRECTORY in findRemovedStack When inferring removed stacks, we use use the fact that a specification doesn't exist on disk to decide to remove the corresponding stack. Checking for the specification on disk was not using `STACKCTL_DIRECTORY`, so if one were set this would always report the file as missing and could result in deleting a stack we should not. Ideally, I would have added a failing test before fixing this, but this area is just not easy to get a test on at this time. --- src/Stackctl/RemovedStack.hs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Stackctl/RemovedStack.hs b/src/Stackctl/RemovedStack.hs index c55e05e..12c2ace 100644 --- a/src/Stackctl/RemovedStack.hs +++ b/src/Stackctl/RemovedStack.hs @@ -9,6 +9,7 @@ import Control.Monad.Trans.Maybe (MaybeT (..), runMaybeT) import Stackctl.AWS.CloudFormation import Stackctl.AWS.Core as AWS import Stackctl.AWS.Scope +import Stackctl.DirectoryOption import Stackctl.FilterOption import UnliftIO.Directory (doesFileExist) @@ -17,26 +18,30 @@ inferRemovedStacks , MonadAWS m , MonadReader env m , HasAwsScope env + , HasDirectoryOption env , HasFilterOption env ) => m [Stack] inferRemovedStacks = do scope <- view awsScopeL paths <- view $ filterOptionL . to filterOptionToPaths - catMaybes <$> traverse (findRemovedStack scope) paths + dir <- view $ directoryOptionL . to unDirectoryOption + catMaybes <$> traverse (findRemovedStack scope dir) paths findRemovedStack :: (MonadUnliftIO m, MonadAWS m) => AwsScope -> FilePath + -- ^ Root directory + -> FilePath -> m (Maybe Stack) -findRemovedStack scope path = runMaybeT $ do +findRemovedStack scope dir path = runMaybeT $ do -- The filter is a full path to a specification in the current -- account/region... stackName <- hoistMaybe $ awsScopeSpecStackName scope path -- that no longer exists... - guard . not =<< doesFileExist path + guard . not =<< doesFileExist (dir path) -- but the Stack it would point to does MaybeT $ awsCloudFormationDescribeStackMaybe stackName From e785b7e6fe53f469377df52d75746c3cbd3207c0 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Mon, 8 Apr 2024 15:16:04 -0400 Subject: [PATCH 107/187] Version bump --- CHANGELOG.md | 6 +++++- package.yaml | 2 +- stackctl.cabal | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f934677..f1c63f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ -## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.6.1.0...main) +## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.6.1.1...main) + +## [v1.6.1.1](https://github.com/freckle/stackctl/compare/v1.6.1.0...v1.6.1.1) + +- Fix: finding removed stacks now respects `STACKCTL_DIRECTORY` ## [v1.6.1.0](https://github.com/freckle/stackctl/compare/v1.6.0.0...v1.6.1.0) diff --git a/package.yaml b/package.yaml index ff0b643..2b37fcc 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: stackctl -version: 1.6.1.0 +version: 1.6.1.1 github: freckle/stackctl license: MIT author: Freckle Engineering diff --git a/stackctl.cabal b/stackctl.cabal index abd64b6..1769c59 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -5,7 +5,7 @@ cabal-version: 1.18 -- see: https://github.com/sol/hpack name: stackctl -version: 1.6.1.0 +version: 1.6.1.1 description: Please see homepage: https://github.com/freckle/stackctl#readme bug-reports: https://github.com/freckle/stackctl/issues From 0d16831cc691140aa4ba5333ec4370383c7943c9 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Fri, 3 May 2024 10:53:09 -0400 Subject: [PATCH 108/187] Require Blammo-1.2.2.3 Works around a bug in `fast-logger` to ensure log messages are actually flushed when `flushLogger` is used. --- CHANGELOG.md | 6 +++++- package.yaml | 4 ++-- stack.yaml | 2 +- stack.yaml.lock | 8 ++++---- stackctl.cabal | 2 +- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1c63f2..80776f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ -## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.6.1.1...main) +## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.6.1.2...main) + +## [v1.6.1.2](https://github.com/freckle/stackctl/compare/v1.6.1.1...v1.6.1.2) + +- Require Blammo-1.2.2.3 ## [v1.6.1.1](https://github.com/freckle/stackctl/compare/v1.6.1.0...v1.6.1.1) diff --git a/package.yaml b/package.yaml index 2b37fcc..88b4090 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: stackctl -version: 1.6.1.1 +version: 1.6.1.2 github: freckle/stackctl license: MIT author: Freckle Engineering @@ -59,7 +59,7 @@ default-extensions: library: source-dirs: src dependencies: - - Blammo >= 1.1.2.1 # getColorsLogger, etc + - Blammo >= 1.1.2.3 # flushLogger bugfix - Glob - QuickCheck - aeson diff --git a/stack.yaml b/stack.yaml index 28029a9..71ce554 100644 --- a/stack.yaml +++ b/stack.yaml @@ -1,6 +1,6 @@ resolver: lts-22.6 extra-deps: - - Blammo-1.1.2.1 + - Blammo-1.1.2.3 - amazonka-mtl-0.1.1.0 - cfn-flip-0.1.0.3 diff --git a/stack.yaml.lock b/stack.yaml.lock index e296fd2..cb89bf7 100644 --- a/stack.yaml.lock +++ b/stack.yaml.lock @@ -5,12 +5,12 @@ packages: - completed: - hackage: Blammo-1.1.2.1@sha256:b74d553fb3557bb10381b806bd34b8bad0b800883f02dfd1cc847f58db40958c,4084 + hackage: Blammo-1.1.2.3@sha256:33112de7280df78009ced5e815907ef62f902b8165434f538ce584df6cd9e47a,4710 pantry-tree: - sha256: bd28931f07beaaae8565a87d8c3b55d3e9ff5c332ae93dc32c1090a4c814e620 - size: 1567 + sha256: ec4524c3153eeb54a8554b3280e00011b21374e36df320733d0c35b8da0c9f23 + size: 1651 original: - hackage: Blammo-1.1.2.1 + hackage: Blammo-1.1.2.3 - completed: hackage: amazonka-mtl-0.1.1.0@sha256:90b45a950c0e398b0e48d1447766f331c2ac3d5a72e15be2bf0be3b3c56159c3,6572 pantry-tree: diff --git a/stackctl.cabal b/stackctl.cabal index 1769c59..ebf1b86 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -98,7 +98,7 @@ library TypeFamilies ghc-options: -fignore-optim-changes -fwrite-ide-info -Weverything -Wno-all-missed-specialisations -Wno-missed-specialisations -Wno-missing-import-lists -Wno-missing-kind-signatures -Wno-missing-local-signatures -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module -Wno-unsafe -optP-Wno-nonportable-include-path build-depends: - Blammo >=1.1.2.1 + Blammo >=1.1.2.3 , Glob , QuickCheck , aeson From be1cf1a8aa884596a7bf1026526bc85cd99a9420 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Fri, 3 May 2024 11:04:11 -0400 Subject: [PATCH 109/187] Update Blammo extra-dep in older resolver --- stack-lts-20.4.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stack-lts-20.4.yaml b/stack-lts-20.4.yaml index 559296b..50d806a 100644 --- a/stack-lts-20.4.yaml +++ b/stack-lts-20.4.yaml @@ -1,7 +1,7 @@ resolver: lts-20.4 extra-deps: - - Blammo-1.1.2.1 + - Blammo-1.1.2.3 - cfn-flip-0.1.0.3 - unliftio-0.2.25.0 From baf618a2981ae1fe1513517d5d642ed15214a463 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Fri, 3 May 2024 12:23:08 -0400 Subject: [PATCH 110/187] Install stack on macOS release Job --- .github/workflows/release.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8af3273..ab13815 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,6 +41,11 @@ jobs: runs-on: ${{ matrix.os }} steps: + # stack was removed in macOS-14 which is now latest + # https://discourse.haskell.org/t/github-hosted-runner-for-macos-aarch64/8717/16 + - if: ${{ runner.os == 'macOS' }} + run: curl -sSL https://get.haskellstack.org/ | sh + - uses: actions/checkout@v4 - uses: freckle/stack-cache-action@v2 - run: echo "$HOME/.local/share/gem/ruby/3.0.0/bin" >>"$GITHUB_PATH" From 3597e6b56b980be932aeb8a01b62a0a8bdc4d325 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 7 May 2024 10:59:14 -0400 Subject: [PATCH 111/187] Commit forgotten cabal update --- stackctl.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackctl.cabal b/stackctl.cabal index ebf1b86..20a0596 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -5,7 +5,7 @@ cabal-version: 1.18 -- see: https://github.com/sol/hpack name: stackctl -version: 1.6.1.1 +version: 1.6.1.2 description: Please see homepage: https://github.com/freckle/stackctl#readme bug-reports: https://github.com/freckle/stackctl/issues From 7463d77c7c07c73955d4c53e562e072efb2d6cd2 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 7 May 2024 10:59:31 -0400 Subject: [PATCH 112/187] Fixup docs --- src/Stackctl/StackSpecYaml.hs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Stackctl/StackSpecYaml.hs b/src/Stackctl/StackSpecYaml.hs index bc13edd..4f714a3 100644 --- a/src/Stackctl/StackSpecYaml.hs +++ b/src/Stackctl/StackSpecYaml.hs @@ -4,18 +4,24 @@ -- Template: -- -- Depends: --- - +-- - -- -- Parameters: --- - ParameterKey: --- ParameterValue: +-- - ParameterKey: +-- ParameterValue: +-- +-- # Or +-- : -- -- Capabilities: --- - +-- - -- -- Tags: --- - Key: --- Value: +-- - Key: +-- Value: +-- +-- # Or +-- : -- @ module Stackctl.StackSpecYaml ( StackSpecYaml (..) From 0d0dfe79c66f10aca51e4e4923543bb02d528548 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 7 May 2024 12:07:34 -0400 Subject: [PATCH 113/187] Extend ParameterValue to support Numbers and Booleans CloudFormation parameters in templates, can either be `String` or `Number`. It's common convention to support "booleans" as well by using `String` with `AllowedValues: [True, False]`. CloudFormation parameters _when ultimately passed as part of a deploy_ must be strings. This causes some edge cases, such as a stringified `Number` being passed at deploy-time as `"3.0"`, instead of `"3"` and then failing. For this reason, we had some special handling for values that parsed from the actual spec yaml as a number. Even with this present, we still captured the value as a `Text` internally (with special `".0"`-handling pre-applied). This causes user-facing confusion and complexity, since generation would still always be at `Text`, even if the user had entered a numeric literal in their spec yaml. This commit changes that to retain the original type used in the user-provided yaml, and only "cast" things to `Text` when necessary (e.g. when need as a true Amazonka `Parameter`). This meant splitting the `newtype` into an actual `data` type with cases for strings and numbers. To this it adds a boolean case, encoding the `"True|False"` convention described above. Ultimately, this results in user-facing interfaces always using actual string, number, or boolean types, and the necessary string casting pushed further to the edges. This changes the interface of `Generate` to deal in `ParametersYaml` values instead of (Amazonka) `Parameter`s, so this will need a major version bump. --- src/Stackctl/Spec/Capture.hs | 4 +- src/Stackctl/Spec/Generate.hs | 4 +- src/Stackctl/StackSpecYaml.hs | 82 ++++++++++++++++++++++++------ test/Stackctl/StackSpecYamlSpec.hs | 22 -------- 4 files changed, 72 insertions(+), 40 deletions(-) diff --git a/src/Stackctl/Spec/Capture.hs b/src/Stackctl/Spec/Capture.hs index 596f9ef..5187026 100644 --- a/src/Stackctl/Spec/Capture.hs +++ b/src/Stackctl/Spec/Capture.hs @@ -13,6 +13,7 @@ import Stackctl.Config (HasConfig) import Stackctl.DirectoryOption (HasDirectoryOption) import Stackctl.Spec.Generate import Stackctl.StackSpec +import Stackctl.StackSpecYaml (parameterYaml, parametersYaml) import System.FilePath.Glob data CaptureOptions = CaptureOptions @@ -103,7 +104,8 @@ runCapture CaptureOptions {..} = do { gDescription = stackDescription stack , gDepends = scoDepends , gActions = Nothing - , gParameters = parameters stack + , gParameters = + parametersYaml . mapMaybe parameterYaml <$> parameters stack , gCapabilities = capabilities stack , gTags = tags stack , gSpec = case path of diff --git a/src/Stackctl/Spec/Generate.hs b/src/Stackctl/Spec/Generate.hs index c3b22db..e920bae 100644 --- a/src/Stackctl/Spec/Generate.hs +++ b/src/Stackctl/Spec/Generate.hs @@ -22,7 +22,7 @@ data Generate = Generate { gDescription :: Maybe StackDescription , gDepends :: Maybe [StackName] , gActions :: Maybe [Action] - , gParameters :: Maybe [Parameter] + , gParameters :: Maybe ParametersYaml , gCapabilities :: Maybe [Capability] , gTags :: Maybe [Tag] , gSpec :: GenerateSpec @@ -81,7 +81,7 @@ generate Generate {..} = do , ssyTemplate = templatePath , ssyDepends = gDepends , ssyActions = gActions - , ssyParameters = parametersYaml . mapMaybe parameterYaml <$> gParameters + , ssyParameters = gParameters , ssyCapabilities = gCapabilities , ssyTags = tagsYaml . map TagYaml <$> gTags } diff --git a/src/Stackctl/StackSpecYaml.hs b/src/Stackctl/StackSpecYaml.hs index 4f714a3..7ae052c 100644 --- a/src/Stackctl/StackSpecYaml.hs +++ b/src/Stackctl/StackSpecYaml.hs @@ -30,7 +30,11 @@ module Stackctl.StackSpecYaml , unParametersYaml , ParameterYaml , parameterYaml + , mkParameterYaml , unParameterYaml + , ParameterValue + , parameterValueFromText + , parameterValueTemplate , TagsYaml , tagsYaml , unTagsYaml @@ -45,6 +49,7 @@ import qualified Data.Aeson.Key as Key import qualified Data.Aeson.KeyMap as KeyMap import Data.Aeson.Types (typeMismatch) import qualified Data.HashMap.Strict as HashMap +import Data.List.Extra (dropSuffix) import Data.Monoid (Last (..)) import qualified Data.Text as T import Stackctl.AWS @@ -123,30 +128,80 @@ instance FromJSON ParameterYaml where parameterYamlPair :: KeyValue kv => ParameterYaml -> kv parameterYamlPair ParameterYaml {..} = pyKey .= pyValue -mkParameterYaml :: Text -> Maybe ParameterValue -> ParameterYaml -mkParameterYaml k = ParameterYaml (Key.fromText k) . Last - parameterYaml :: Parameter -> Maybe ParameterYaml parameterYaml p = do k <- p ^. parameter_parameterKey let mv = p ^. parameter_parameterValue - pure $ mkParameterYaml k $ ParameterValue <$> mv + pure $ mkParameterYaml k $ parameterValueFromText <$> mv + +mkParameterYaml :: Text -> Maybe ParameterValue -> ParameterYaml +mkParameterYaml k = ParameterYaml (Key.fromText k) . Last unParameterYaml :: ParameterYaml -> Parameter unParameterYaml (ParameterYaml k v) = - makeParameter (Key.toText k) $ unParameterValue <$> getLast v + makeParameter (Key.toText k) $ parameterValueToText <$> getLast v -newtype ParameterValue = ParameterValue - { unParameterValue :: Text - } +data ParameterValue + = StringParameter Text + | NumberParameter Double + | -- | Encodes as String True|False + BooleanParameter Bool deriving stock (Eq, Show) - deriving newtype (Semigroup, ToJSON) instance FromJSON ParameterValue where parseJSON = \case - String x -> pure $ ParameterValue x - Number x -> pure $ ParameterValue $ dropSuffix ".0" $ pack $ show x - x -> fail $ "Expected String or Number, got: " <> show x + String t -> pure $ StringParameter t + Number s -> pure $ NumberParameter $ realToFrac s + Bool b -> pure $ BooleanParameter b + x -> typeMismatch "String, Number or Bool" x + +instance ToJSON ParameterValue where + toJSON = \case + StringParameter t -> toJSON t + NumberParameter d -> toJSON d + BooleanParameter b -> toJSON $ pack $ show b + toEncoding = \case + StringParameter t -> toEncoding t + NumberParameter d -> toEncoding d + BooleanParameter b -> toEncoding $ pack $ show b + +parameterValueToText :: ParameterValue -> Text +parameterValueToText = \case + StringParameter t -> t + NumberParameter d -> pack $ dropSuffix ".0" $ show d + BooleanParameter b -> pack $ show b + +parameterValueFromText :: Text -> ParameterValue +parameterValueFromText = \case + v | T.toLower v == "true" -> BooleanParameter True + v | T.toLower v == "false" -> BooleanParameter False + v | Just d <- readMaybe (unpack v) -> NumberParameter d + v -> StringParameter v + +-- | For use as the value in a @Parameters@ object of a CFN Template +parameterValueTemplate :: ParameterValue -> Value +parameterValueTemplate v = + object + $ catMaybes + [ Just $ "Type" .= parameterValueType v + , Just $ "Default" .= parameterValueDefault v + , ("AllowedValues" .=) <$> parameterValueAllowedValues v + ] + +parameterValueType :: ParameterValue -> Text +parameterValueType = \case + StringParameter {} -> "String" + NumberParameter {} -> "Number" + BooleanParameter {} -> "String" + +parameterValueDefault :: ParameterValue -> Value +parameterValueDefault = toJSON + +parameterValueAllowedValues :: ParameterValue -> Maybe Value +parameterValueAllowedValues = \case + StringParameter {} -> Nothing + NumberParameter {} -> Nothing + BooleanParameter {} -> Just $ toJSON [String "True", String "False"] newtype TagsYaml = TagsYaml { unTagsYaml :: [TagYaml] @@ -200,6 +255,3 @@ instance FromJSON TagYaml where tagYamlPair :: KeyValue kv => TagYaml -> kv tagYamlPair (TagYaml t) = Key.fromText (t ^. tag_key) .= (t ^. tag_value) - -dropSuffix :: Text -> Text -> Text -dropSuffix suffix t = fromMaybe t $ T.stripSuffix suffix t diff --git a/test/Stackctl/StackSpecYamlSpec.hs b/test/Stackctl/StackSpecYamlSpec.hs index e67ead3..1dd9f09 100644 --- a/test/Stackctl/StackSpecYamlSpec.hs +++ b/test/Stackctl/StackSpecYamlSpec.hs @@ -80,28 +80,6 @@ spec = do param ^. parameter_parameterKey `shouldBe` Just "Pie" param ^. parameter_parameterValue `shouldBe` Just "3.14" - it "has informative errors" $ do - let Left ex = - Yaml.decodeEither' @StackSpecYaml - $ mconcat - [ "Template: foo.yaml\n" - , "Parameters:\n" - , " - ParameterKey: Norway\n" - , " ParameterValue: no\n" - ] - - show ex - `shouldBe` "AesonException \"Error in $.Parameters[0].ParameterValue: Expected String or Number, got: Bool False\"" - - it "has informative errors in Object form" $ do - let Left ex = - Yaml.decodeEither' @StackSpecYaml - $ mconcat - ["Template: foo.yaml\n", "Parameters:\n", " Norway: no\n"] - - show ex - `shouldBe` "AesonException \"Error in $.Parameters.Norway: Expected String or Number, got: Bool False\"" - it "handles null Value" $ do StackSpecYaml {..} <- Yaml.decodeThrow From 35e621d1261d5b10033c533e9550a6c7cbc8e2bf Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 7 May 2024 12:18:22 -0400 Subject: [PATCH 114/187] Version bump --- CHANGELOG.md | 10 +++++++++- package.yaml | 2 +- stackctl.cabal | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80776f0..f778df7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,12 @@ -## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.6.1.2...main) +## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.7.0.0...main) + +## [v1.7.0.0](https://github.com/freckle/stackctl/compare/v1.6.1.2...v1.7.0.0) + +- Retain numeric parameter types, and add boolean parameter handling, in our own + yaml generation + + This required changing `Generate` to use `ParametersYaml` instead of + `[Parameter]`, hence the major version bump. ## [v1.6.1.2](https://github.com/freckle/stackctl/compare/v1.6.1.1...v1.6.1.2) diff --git a/package.yaml b/package.yaml index 88b4090..e6d76f2 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: stackctl -version: 1.6.1.2 +version: 1.7.0.0 github: freckle/stackctl license: MIT author: Freckle Engineering diff --git a/stackctl.cabal b/stackctl.cabal index 20a0596..9fb6bd8 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -5,7 +5,7 @@ cabal-version: 1.18 -- see: https://github.com/sol/hpack name: stackctl -version: 1.6.1.2 +version: 1.7.0.0 description: Please see homepage: https://github.com/freckle/stackctl#readme bug-reports: https://github.com/freckle/stackctl/issues From 095d9b1d28da399a4d202116a180ecbd537cbfd4 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 7 May 2024 12:56:23 -0400 Subject: [PATCH 115/187] Remove Generate type At this point, the type was exactly a `StackSpecYaml`, which makes sense given it's purpose. By moving the only fields that weren't part of that to positional arguments, we can simplify it away. --- src/Stackctl/Spec/Capture.hs | 46 +++++++++++++++++++++-------------- src/Stackctl/Spec/Generate.hs | 39 ++++++++--------------------- 2 files changed, 38 insertions(+), 47 deletions(-) diff --git a/src/Stackctl/Spec/Capture.hs b/src/Stackctl/Spec/Capture.hs index 5187026..2332edd 100644 --- a/src/Stackctl/Spec/Capture.hs +++ b/src/Stackctl/Spec/Capture.hs @@ -13,7 +13,13 @@ import Stackctl.Config (HasConfig) import Stackctl.DirectoryOption (HasDirectoryOption) import Stackctl.Spec.Generate import Stackctl.StackSpec -import Stackctl.StackSpecYaml (parameterYaml, parametersYaml) +import Stackctl.StackSpecYaml + ( StackSpecYaml (..) + , TagYaml (..) + , parameterYaml + , parametersYaml + , tagsYaml + ) import System.FilePath.Glob data CaptureOptions = CaptureOptions @@ -92,7 +98,7 @@ runCapture CaptureOptions {..} = do setScopeName scope = maybe scope (\name -> scope {awsAccountName = name}) scoAccountName - generate' stack template path templatePath = do + generate' stack template mPath mTemplatePath = do let stackName = StackName $ stack ^. stack_stackName templateBody = templateBodyFromValue template @@ -100,22 +106,26 @@ runCapture CaptureOptions {..} = do void $ local (awsScopeL %~ setScopeName) $ generate - Generate - { gDescription = stackDescription stack - , gDepends = scoDepends - , gActions = Nothing - , gParameters = - parametersYaml . mapMaybe parameterYaml <$> parameters stack - , gCapabilities = capabilities stack - , gTags = tags stack - , gSpec = case path of - Nothing -> GenerateSpec stackName - Just sp -> GenerateSpecTo stackName sp - , gTemplate = case templatePath of - Nothing -> GenerateTemplate templateBody scoTemplateFormat - Just tp -> GenerateTemplateTo templateBody tp - , gOverwrite = False - } + False + ( case mPath of + Nothing -> GenerateSpec stackName + Just sp -> GenerateSpecTo stackName sp + ) + ( case mTemplatePath of + Nothing -> GenerateTemplate templateBody scoTemplateFormat + Just tp -> GenerateTemplateTo templateBody tp + ) + ( \templatePath -> + StackSpecYaml + { ssyDescription = stackDescription stack + , ssyTemplate = templatePath + , ssyDepends = scoDepends + , ssyActions = Nothing + , ssyParameters = parametersYaml . mapMaybe parameterYaml <$> parameters stack + , ssyCapabilities = capabilities stack + , ssyTags = tagsYaml . map TagYaml <$> tags stack + } + ) results <- awsCloudFormationGetStackNamesMatching scoStackName diff --git a/src/Stackctl/Spec/Generate.hs b/src/Stackctl/Spec/Generate.hs index e920bae..1193f27 100644 --- a/src/Stackctl/Spec/Generate.hs +++ b/src/Stackctl/Spec/Generate.hs @@ -1,6 +1,5 @@ module Stackctl.Spec.Generate - ( Generate (..) - , GenerateSpec (..) + ( GenerateSpec (..) , GenerateTemplate (..) , generate , TemplateFormat (..) @@ -18,18 +17,6 @@ import Stackctl.StackSpec import Stackctl.StackSpecPath import Stackctl.StackSpecYaml -data Generate = Generate - { gDescription :: Maybe StackDescription - , gDepends :: Maybe [StackName] - , gActions :: Maybe [Action] - , gParameters :: Maybe ParametersYaml - , gCapabilities :: Maybe [Capability] - , gTags :: Maybe [Tag] - , gSpec :: GenerateSpec - , gTemplate :: GenerateTemplate - , gOverwrite :: Bool - } - data GenerateSpec = -- | Generate at an inferred name GenerateSpec StackName @@ -57,15 +44,18 @@ generate , HasAwsScope env , HasDirectoryOption env ) - => Generate + => Bool + -> GenerateSpec + -> GenerateTemplate + -> (FilePath -> StackSpecYaml) -> m FilePath -generate Generate {..} = do +generate overwrite spec template toStackSpecYaml = do let - (stackName, stackPath) = case gSpec of + (stackName, stackPath) = case spec of GenerateSpec name -> (name, unpack (unStackName name) <> ".yaml") GenerateSpecTo name path -> (name, path) - (mTemplateBody, templatePath) = case gTemplate of + (mTemplateBody, templatePath) = case template of GenerateTemplate body format -> ( Just body , case format of @@ -75,21 +65,12 @@ generate Generate {..} = do GenerateTemplateTo body path -> (Just body, path) UseExistingTemplate path -> (Nothing, path) - specYaml = - StackSpecYaml - { ssyDescription = gDescription - , ssyTemplate = templatePath - , ssyDepends = gDepends - , ssyActions = gActions - , ssyParameters = gParameters - , ssyCapabilities = gCapabilities - , ssyTags = tagsYaml . map TagYaml <$> gTags - } + specYaml = toStackSpecYaml templatePath dir <- view $ directoryOptionL . to unDirectoryOption specPath <- buildSpecPath stackName stackPath stackSpec <- buildStackSpec dir specPath specYaml withThreadContext ["stackName" .= stackSpecStackName stackSpec] $ do - writeStackSpec gOverwrite stackSpec mTemplateBody + writeStackSpec overwrite stackSpec mTemplateBody pure $ stackSpecPathFilePath specPath From d2a62e40cbc4df1d81f6057850e4ba860176ee46 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 7 May 2024 12:57:39 -0400 Subject: [PATCH 116/187] fixup! Version bump --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f778df7..710073d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,9 @@ - Retain numeric parameter types, and add boolean parameter handling, in our own yaml generation - This required changing `Generate` to use `ParametersYaml` instead of - `[Parameter]`, hence the major version bump. + This required changing the `Generate` to use `ParametersYaml` instead of + `[Parameter]`, and ultimately led us to removing it, hence the major version + bump. ## [v1.6.1.2](https://github.com/freckle/stackctl/compare/v1.6.1.1...v1.6.1.2) From a7ef12ca3d8cd1f682501f513c6222d495042d30 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 7 May 2024 13:24:08 -0400 Subject: [PATCH 117/187] Remove redundant import --- src/Stackctl/Spec/Generate.hs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Stackctl/Spec/Generate.hs b/src/Stackctl/Spec/Generate.hs index 1193f27..b6a68e5 100644 --- a/src/Stackctl/Spec/Generate.hs +++ b/src/Stackctl/Spec/Generate.hs @@ -9,7 +9,6 @@ import Stackctl.Prelude import Stackctl.AWS import Stackctl.AWS.Scope -import Stackctl.Action import Stackctl.Config (HasConfig) import Stackctl.DirectoryOption import Stackctl.Spec.Discover (buildSpecPath) From bcf790277fd62168be8f6aaefc1ca215fcf934fa Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Thu, 23 May 2024 13:15:31 -0400 Subject: [PATCH 118/187] Add note to README about AWS CloudFormation Git Sync --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.md b/README.md index 8b61894..7da76a3 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,33 @@ Once installed, see: The man pages are also available [online](https://freckle.github.io/stackctl/), but contain documentation as of `main`, and not your installed version. +## Comparison to AWS CloudFormation Git Sync + +[AWS CloudFormation Git Sync][aws-git-sync] was recently released by AWS. It +allows you to link a repository on GitHub to a CloudFormation Stack. The +repository contains a "deployment file" that defines a `template-file-path`, +`parameters`, and `tags` -- effectively, a Stack Specification. + +When AWS notices updates to the deployment or template file land on a defined +branch, it updates the configured Stack accordingly, emitting events to SNS as +it does. + +This is great for simple use-cases, and we fully expect they'll improve and +extend it such that it obviates Stackctl one day. In the meantime, there are +currently the following limitations when compared to Stackctl: + +1. A repository can only target a single account and region +1. There is no changeset flow amenable to previewing changes via PRs. You update + the file(s) on `main` and it syncs, that's it. If you're using a PR, you have + only linting and human review as possible pre-deployment steps. +1. There is no way to specify description, capabilities, or dependencies +1. As of 12/23, there seemed to be some bugs, and the setup installs a managed + event bridge that "phones home", sending events about your updates to some + other AWS account ([source][first-look-blog]) + +[aws-git-sync]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/git-sync.html +[first-look-blog]: https://medium.com/@mattgillard/first-look-git-sync-for-cloudformation-stacks-9e2f39c311ac + ## Relationship to CloudGenesis [CloudGenesis][] is a project that also takes a directory of Stack From 79578f8f48c588bf78a507b94db54cadd542791e Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 3 Sep 2024 10:57:41 -0400 Subject: [PATCH 119/187] Add withAssumedRole, deprecate assumeRole `assumeRole` does not spawn a background thread to refresh credentials. This means that if the block goes on long enough, expired credentials errors will start happening. `withAssumedRole` addresses this. I chose to make a new function under a different name for two reasons: 1. The fixed function incurs a `MonadUnliftIO` constraint. Users could stay on the deprecated function if they're not able to quickly organize things to satisfy this constraint. 2. The naming better matches the analogous Amazonka function[^1]. [^1]: https://hackage.haskell.org/package/amazonka-2.0/docs/Amazonka-Auth-STS.html#v:fromAssumedRole --- src/Stackctl/AWS/Core.hs | 36 +++++++++++++++++++++++++++++++++++- stackctl.cabal | 2 +- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/Stackctl/AWS/Core.hs b/src/Stackctl/AWS/Core.hs index 78b8931..df3b72c 100644 --- a/src/Stackctl/AWS/Core.hs +++ b/src/Stackctl/AWS/Core.hs @@ -9,7 +9,7 @@ module Stackctl.AWS.Core -- * "Control.Monad.AWS" extensions , simple , discover - , assumeRole + , withAssumedRole -- * Error-handling , handlingServiceError @@ -22,6 +22,9 @@ module Stackctl.AWS.Core , Region (..) , FromText (..) , ToText (..) + + -- * Deprecated + , assumeRole ) where import Stackctl.Prelude @@ -29,6 +32,7 @@ import Stackctl.Prelude import Amazonka ( AWSRequest , AWSResponse + , Env' (auth) , Region , ServiceError , serviceError_code @@ -38,6 +42,7 @@ import Amazonka , _ServiceError ) import qualified Amazonka +import Amazonka.Auth.Background (fetchAuthInBackground) import Amazonka.Auth.Keys (fromSession) import Amazonka.Data.Text (FromText (..), ToText (..)) import qualified Amazonka.Env as Amazonka @@ -87,6 +92,11 @@ simple req post = do maybe (throwString err) pure $ post resp +-- | Use 'withAssumedRole' instead +-- +-- This function is like 'withAssumedRole' except it doesn't spawn a background +-- thread to keep credentials refreshed. You make encounter expired credentials +-- if the block used under 'assumeRole' goes for long enough. assumeRole :: (MonadIO m, MonadAWS m) => Text @@ -111,6 +121,30 @@ assumeRole role sessionName f = do pure $ fromSession accessKeyId secretAccessKey sessionToken localEnv assumeEnv f +{-# DEPRECATED assumeRole "Use withAssumedRole instead" #-} + +-- | Assume a role using the @sts:AssumeRole@ API and run an action +withAssumedRole + :: (MonadUnliftIO m, MonadAWS m) + => Text + -- ^ Role ARN + -> Text + -- ^ Role session name + -> m a + -- ^ Action to run as the assumed role + -> m a +withAssumedRole roleArn roleSessionName f = do + keys <- withRunInIO $ \runInIO -> do + let getCredentials = do + resp <- + runInIO + $ send + $ newAssumeRole roleArn roleSessionName + pure $ resp ^. assumeRoleResponse_credentials + + fetchAuthInBackground getCredentials + + localEnv (\env -> env {auth = Identity keys}) f newtype AccountId = AccountId { unAccountId :: Text diff --git a/stackctl.cabal b/stackctl.cabal index 9fb6bd8..423e9da 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -1,6 +1,6 @@ cabal-version: 1.18 --- This file has been generated from package.yaml by hpack version 0.36.0. +-- This file has been generated from package.yaml by hpack version 0.37.0. -- -- see: https://github.com/sol/hpack From 2f440d770b68732d4019d6d6dfc034c33ddb3c03 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 3 Sep 2024 11:01:39 -0400 Subject: [PATCH 120/187] Version bump --- CHANGELOG.md | 6 +++++- package.yaml | 2 +- stackctl.cabal | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 710073d..4b7de9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ -## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.7.0.0...main) +## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.7.1.0...main) + +## [v1.7.1.0](https://github.com/freckle/stackctl/compare/v1.7.0.0...v1.7.1.0) + +- Add `withAssumedRole`, deprecate `assumeRole` ## [v1.7.0.0](https://github.com/freckle/stackctl/compare/v1.6.1.2...v1.7.0.0) diff --git a/package.yaml b/package.yaml index e6d76f2..dcecb8b 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: stackctl -version: 1.7.0.0 +version: 1.7.1.0 github: freckle/stackctl license: MIT author: Freckle Engineering diff --git a/stackctl.cabal b/stackctl.cabal index 423e9da..b80be77 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -5,7 +5,7 @@ cabal-version: 1.18 -- see: https://github.com/sol/hpack name: stackctl -version: 1.7.0.0 +version: 1.7.1.0 description: Please see homepage: https://github.com/freckle/stackctl#readme bug-reports: https://github.com/freckle/stackctl/issues From c9ba1ad1cb65e6bcccf7824a19bfe1a62c0c8af1 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 3 Sep 2024 11:02:53 -0400 Subject: [PATCH 121/187] Try to specify v14 of fourmolu --- .restyled.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.restyled.yaml b/.restyled.yaml index 58e337b..0c5fd2d 100644 --- a/.restyled.yaml +++ b/.restyled.yaml @@ -2,7 +2,9 @@ restylers_version: dev restylers: - cabal-fmt: enabled: false - - fourmolu + - fourmolu: + image: + tag: v14 - stylish-haskell: enabled: false - prettier-markdown: From c2a4ba0c2f343ece7b8d4932175d084b551a2f72 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 3 Sep 2024 11:04:55 -0400 Subject: [PATCH 122/187] Specify full fourmolu image --- .restyled.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.restyled.yaml b/.restyled.yaml index 0c5fd2d..20a1cec 100644 --- a/.restyled.yaml +++ b/.restyled.yaml @@ -4,7 +4,7 @@ restylers: enabled: false - fourmolu: image: - tag: v14 + tag: v0.14.1.0 - stylish-haskell: enabled: false - prettier-markdown: From 9c0e43f22862343c43605841681ff634bd7164fd Mon Sep 17 00:00:00 2001 From: Pat Brisbin Date: Tue, 3 Sep 2024 16:47:12 -0400 Subject: [PATCH 123/187] Update src/Stackctl/AWS/Core.hs --- src/Stackctl/AWS/Core.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Stackctl/AWS/Core.hs b/src/Stackctl/AWS/Core.hs index df3b72c..dba4001 100644 --- a/src/Stackctl/AWS/Core.hs +++ b/src/Stackctl/AWS/Core.hs @@ -95,7 +95,7 @@ simple req post = do -- | Use 'withAssumedRole' instead -- -- This function is like 'withAssumedRole' except it doesn't spawn a background --- thread to keep credentials refreshed. You make encounter expired credentials +-- thread to keep credentials refreshed. You may encounter expired credentials -- if the block used under 'assumeRole' goes for long enough. assumeRole :: (MonadIO m, MonadAWS m) From 3e2b5632538693fb193db964eae66542d9ba6d73 Mon Sep 17 00:00:00 2001 From: Pat Brisbin Date: Mon, 16 Sep 2024 14:25:13 -0400 Subject: [PATCH 124/187] Update README.md --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7da76a3..720e194 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ to-be-deployed) CloudFormation Stacks including the Template, Parameters, and 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 @@ This project also contains a Haskell library for doing the same. 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 From 43624eed809f01d832eae16ae6eab6ae8e6b6700 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 11 Sep 2024 09:14:37 -0400 Subject: [PATCH 125/187] Ignore Functor law HLint --- .hlint.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.hlint.yaml b/.hlint.yaml index 7bc8214..5b48bc7 100644 --- a/.hlint.yaml +++ b/.hlint.yaml @@ -11,6 +11,7 @@ - ignore: {name: "Use join"} # this often leads to cryptic code when do notation is easier to read - ignore: {name: "Redundant ^."} # commonly broken by esqueleto - ignore: {name: "Use ++"} # less readable for commandline option lists +- ignore: {name: "Functor law"} # too aggressive # Custom Warnings - warn: {lhs: mapM, rhs: traverse} From 53f8bbf6b3a9bf53441bda6f8cdeb2c32b4b9bdf Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Mon, 23 Sep 2024 11:09:06 -0400 Subject: [PATCH 126/187] Track ChangeSetType in our ChangeSet --- src/Stackctl/AWS/CloudFormation.hs | 16 ++++++++++------ test/Stackctl/Spec/Changes/FormatSpec.hs | 3 ++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Stackctl/AWS/CloudFormation.hs b/src/Stackctl/AWS/CloudFormation.hs index 0a5ecc9..a6bd65b 100644 --- a/src/Stackctl/AWS/CloudFormation.hs +++ b/src/Stackctl/AWS/CloudFormation.hs @@ -330,6 +330,7 @@ data ChangeSet = ChangeSet { csCreationTime :: UTCTime , csChanges :: Maybe [Change] , csChangeSetName :: ChangeSetName + , csChangeSetType :: ChangeSetType , csExecutionStatus :: ExecutionStatus , csChangeSetId :: ChangeSetId , csParameters :: Maybe [Parameter] @@ -342,12 +343,14 @@ data ChangeSet = ChangeSet , 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) @@ -415,16 +418,17 @@ awsCloudFormationCreateChangeSet stackName mStackDescription stackTemplate param 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 diff --git a/test/Stackctl/Spec/Changes/FormatSpec.hs b/test/Stackctl/Spec/Changes/FormatSpec.hs index e04550d..2cdbbe9 100644 --- a/test/Stackctl/Spec/Changes/FormatSpec.hs +++ b/test/Stackctl/Spec/Changes/FormatSpec.hs @@ -5,6 +5,7 @@ where import Stackctl.Prelude +import Amazonka.CloudFormation.Types (ChangeSetType (..)) import Data.Aeson import Stackctl.AWS.CloudFormation (changeSetFromResponse) import Stackctl.Colors @@ -28,7 +29,7 @@ formatChangeSetGolden :: FilePath -> Format -> IO (Golden Text) formatChangeSetGolden path fmt = do actual <- formatChangeSet noColors OmitFull "some-stack" fmt - . (changeSetFromResponse <=< decodeStrict) + . (changeSetFromResponse ChangeSetType_UPDATE <=< decodeStrict) . encodeUtf8 <$> readFileUtf8 path From 90581998a951c5d08095bb05a188e9957f372ab3 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 11 Sep 2024 09:14:45 -0400 Subject: [PATCH 127/187] Cancel UpdateStack operations on Ctl-C ![](https://files.pbrisbin.com/screenshots/screenshot.3323976.png) --- package.yaml | 1 + src/Stackctl/AWS/CloudFormation.hs | 51 +++++++++++++++--------- src/Stackctl/CancelHandler.hs | 33 +++++++++++++++ src/Stackctl/Spec/Deploy.hs | 11 ++++- stackctl.cabal | 3 ++ test/Stackctl/CancelHandlerSpec.hs | 19 +++++++++ test/Stackctl/Spec/Changes/FormatSpec.hs | 3 +- 7 files changed, 99 insertions(+), 22 deletions(-) create mode 100644 src/Stackctl/CancelHandler.hs create mode 100644 test/Stackctl/CancelHandlerSpec.hs diff --git a/package.yaml b/package.yaml index dcecb8b..65f1e97 100644 --- a/package.yaml +++ b/package.yaml @@ -95,6 +95,7 @@ library: - time - transformers - typed-process + - unix - unliftio >= 0.2.25.0 # UnliftIO.Exception.Lens - unordered-containers - uuid diff --git a/src/Stackctl/AWS/CloudFormation.hs b/src/Stackctl/AWS/CloudFormation.hs index a6bd65b..0e21dc0 100644 --- a/src/Stackctl/AWS/CloudFormation.hs +++ b/src/Stackctl/AWS/CloudFormation.hs @@ -41,6 +41,7 @@ module Stackctl.AWS.CloudFormation , awsCloudFormationGetStackNamesMatching , awsCloudFormationGetMostRecentStackEventId , awsCloudFormationDeleteStack + , awsCloudFormationCancelUpdateStack , awsCloudFormationWait , awsCloudFormationGetTemplate @@ -50,6 +51,7 @@ module Stackctl.AWS.CloudFormation , changeSetJSON , ChangeSetId (..) , ChangeSetName (..) + , ChangeSetType (..) , Change (..) , ResourceChange (..) , Replacement (..) @@ -66,6 +68,7 @@ module Stackctl.AWS.CloudFormation 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 @@ import Amazonka.Core , _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 @@ awsCloudFormationDescribeStackEvents stackName mLastId = do let req = newDescribeStackEvents & describeStackEvents_stackName - ?~ unStackName stackName + ?~ unStackName stackName runConduit $ AWS.paginate req @@ -245,7 +248,7 @@ awsCloudFormationGetMostRecentStackEventId stackName = do req = newDescribeStackEvents & describeStackEvents_stackName - ?~ unStackName stackName + ?~ unStackName stackName -- Events are returned most-recent first, so "last" is "first" here getFirstEventId :: [StackEvent] -> Maybe Text @@ -264,15 +267,20 @@ awsCloudFormationDeleteStack => 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) @@ -281,10 +289,8 @@ awsCloudFormationWait 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 @@ awsCloudFormationGetTemplate stackName = do 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 @@ awsCloudFormationGetTemplate stackName = do 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) @@ -404,10 +417,10 @@ awsCloudFormationCreateChangeSet stackName mStackDescription stackTemplate param 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..." diff --git a/src/Stackctl/CancelHandler.hs b/src/Stackctl/CancelHandler.hs new file mode 100644 index 0000000..09a5986 --- /dev/null +++ b/src/Stackctl/CancelHandler.hs @@ -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) (const remove) . const + +-- | 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 diff --git a/src/Stackctl/Spec/Deploy.hs b/src/Stackctl/Spec/Deploy.hs index 53094a7..08295b6 100644 --- a/src/Stackctl/Spec/Deploy.hs +++ b/src/Stackctl/Spec/Deploy.hs @@ -14,6 +14,7 @@ import Options.Applicative 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 @@ deployChangeSet confirmation changeSet = do 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 diff --git a/stackctl.cabal b/stackctl.cabal index b80be77..acd99c8 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -35,6 +35,7 @@ library Stackctl.AWS.Orphans Stackctl.AWS.Scope Stackctl.AWS.STS + Stackctl.CancelHandler Stackctl.CLI Stackctl.ColorOption Stackctl.Colors @@ -135,6 +136,7 @@ library , time , transformers , typed-process + , unix , unliftio >=0.2.25.0 , unordered-containers , uuid @@ -187,6 +189,7 @@ test-suite spec Stackctl.AWS.EC2Spec Stackctl.AWS.LambdaSpec Stackctl.AWS.ScopeSpec + Stackctl.CancelHandlerSpec Stackctl.Config.RequiredVersionSpec Stackctl.ConfigSpec Stackctl.FilterOptionSpec diff --git a/test/Stackctl/CancelHandlerSpec.hs b/test/Stackctl/CancelHandlerSpec.hs new file mode 100644 index 0000000..0ad5679 --- /dev/null +++ b/test/Stackctl/CancelHandlerSpec.hs @@ -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` () diff --git a/test/Stackctl/Spec/Changes/FormatSpec.hs b/test/Stackctl/Spec/Changes/FormatSpec.hs index 2cdbbe9..ca5c8ae 100644 --- a/test/Stackctl/Spec/Changes/FormatSpec.hs +++ b/test/Stackctl/Spec/Changes/FormatSpec.hs @@ -5,9 +5,8 @@ where import Stackctl.Prelude -import Amazonka.CloudFormation.Types (ChangeSetType (..)) 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 ((-<.>)) From 55d5d4a801aefb8ca31bbbe7e97545b3b0d3c3e5 Mon Sep 17 00:00:00 2001 From: Pat Brisbin Date: Mon, 23 Sep 2024 12:21:56 -0400 Subject: [PATCH 128/187] Fix haddocks in CancelHandler --- src/Stackctl/CancelHandler.hs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Stackctl/CancelHandler.hs b/src/Stackctl/CancelHandler.hs index 09a5986..d5ad797 100644 --- a/src/Stackctl/CancelHandler.hs +++ b/src/Stackctl/CancelHandler.hs @@ -9,11 +9,11 @@ import Stackctl.Prelude import System.Posix.Signals --- | Install a 'keyboardSignal handler, run an action, then remove it +-- | Install a 'keyboardSignal' handler, run an action, then remove it with :: MonadUnliftIO m => m a -> m b -> m b with f = bracket (install f) (const remove) . const --- | Install a 'keyboardSignal handler that runs the given action once +-- | Install a 'keyboardSignal' handler that runs the given action once install :: MonadUnliftIO m => m a -> m () install f = do withRunInIO $ \runInIO -> do @@ -22,11 +22,11 @@ install f = do runInIO f void $ installHandler keyboardSignal handler Nothing --- | Remove the current handler for 'keyboardSignal (i.e. install 'Default') +-- | 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 +-- | Trigger the installed 'keyboardSignal' handler -- -- This is used by our test suite. trigger :: MonadIO m => m () From 97bf3d045f43bc623d93c7b55aaba552ee50b49d Mon Sep 17 00:00:00 2001 From: Pat Brisbin Date: Mon, 23 Sep 2024 12:22:08 -0400 Subject: [PATCH 129/187] Use bracket_ --- src/Stackctl/CancelHandler.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Stackctl/CancelHandler.hs b/src/Stackctl/CancelHandler.hs index d5ad797..05ec02e 100644 --- a/src/Stackctl/CancelHandler.hs +++ b/src/Stackctl/CancelHandler.hs @@ -11,7 +11,7 @@ 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) (const remove) . const +with f = bracket_ (install f) remove -- | Install a 'keyboardSignal' handler that runs the given action once install :: MonadUnliftIO m => m a -> m () From 19f680b14d8c24b2d71f8f2e504dc87abe1b85ce Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 24 Sep 2024 10:23:22 -0400 Subject: [PATCH 130/187] Version bump --- CHANGELOG.md | 6 +++++- package.yaml | 2 +- stackctl.cabal | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b7de9b..05c1df4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/package.yaml b/package.yaml index 65f1e97..4661368 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: stackctl -version: 1.7.1.0 +version: 1.7.2.0 github: freckle/stackctl license: MIT author: Freckle Engineering diff --git a/stackctl.cabal b/stackctl.cabal index acd99c8..97fbfb9 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -5,7 +5,7 @@ cabal-version: 1.18 -- see: https://github.com/sol/hpack name: stackctl -version: 1.7.1.0 +version: 1.7.2.0 description: Please see homepage: https://github.com/freckle/stackctl#readme bug-reports: https://github.com/freckle/stackctl/issues From f2ff149c48d026e3292ba79af080df89d64afcec Mon Sep 17 00:00:00 2001 From: "freckle-automation-app[bot]" <176077675+freckle-automation-app[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 19:48:45 +0000 Subject: [PATCH 131/187] Update .github/workflows/mergeabot.yml --- .github/workflows/mergeabot.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/workflows/mergeabot.yml diff --git a/.github/workflows/mergeabot.yml b/.github/workflows/mergeabot.yml new file mode 100644 index 0000000..f1e628a --- /dev/null +++ b/.github/workflows/mergeabot.yml @@ -0,0 +1,19 @@ +name: Mergeabot + +on: + schedule: + - cron: "0 0 * * *" + + pull_request: + +permissions: + contents: write + pull-requests: write + +jobs: + mergeabot: + runs-on: ubuntu-latest + steps: + - uses: freckle/mergeabot-action@v2 + with: + quarantine-days: 5 From 4fcaa5119382e1c3f00781826a4cac13f157e5c8 Mon Sep 17 00:00:00 2001 From: "freckle-automation-app[bot]" <176077675+freckle-automation-app[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 13:53:52 -0800 Subject: [PATCH 132/187] Fix: add-asana-comment Co-authored-by: freckle-automation-app[bot] <176077675+freckle-automation-app[bot]@users.noreply.github.com> --- .github/workflows/add-asana-comment.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .github/workflows/add-asana-comment.yml diff --git a/.github/workflows/add-asana-comment.yml b/.github/workflows/add-asana-comment.yml new file mode 100644 index 0000000..aaa3f6d --- /dev/null +++ b/.github/workflows/add-asana-comment.yml @@ -0,0 +1,16 @@ +name: Asana + +on: + pull_request: + types: [opened] + +jobs: + link-asana-task: + if: ${{ github.actor != 'dependabot[bot]' }} + runs-on: ubuntu-latest + steps: + - uses: Asana/create-app-attachment-github-action@v1.3 + id: postAttachment + with: + asana-secret: ${{ secrets.ASANA_API_ACCESS_KEY }} + - run: echo "Status is ${{ steps.postAttachment.outputs.status }}" From 8966992cc7f852e933876a9089e31b8937fc1f18 Mon Sep 17 00:00:00 2001 From: "freckle-automation-app[bot]" <176077675+freckle-automation-app[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 13:54:20 -0800 Subject: [PATCH 133/187] Update .github/dependabot.yml (#78) Co-authored-by: freckle-automation-app[bot] <176077675+freckle-automation-app[bot]@users.noreply.github.com> --- .github/dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..1230149 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" From 42644c7be854a51ba446fe873c9ab752373e1791 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Dec 2024 11:08:20 -0500 Subject: [PATCH 134/187] Bump actions/configure-pages from 3 to 5 (#81) Bumps [actions/configure-pages](https://github.com/actions/configure-pages) from 3 to 5. - [Release notes](https://github.com/actions/configure-pages/releases) - [Commits](https://github.com/actions/configure-pages/compare/v3...v5) --- updated-dependencies: - dependency-name: actions/configure-pages dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 301e921..92e3529 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -36,7 +36,7 @@ jobs: cp -v man/*.html _site/ cp -v _site/stackctl.1.html _site/index.html - - uses: actions/configure-pages@v3 + - uses: actions/configure-pages@v5 - uses: actions/upload-pages-artifact@v1 with: path: _site From 27bca4430c723ac5a98970c29feaace649afd8ab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 21:55:01 +0000 Subject: [PATCH 135/187] Bump actions/deploy-pages from 1 to 4 Bumps [actions/deploy-pages](https://github.com/actions/deploy-pages) from 1 to 4. - [Release notes](https://github.com/actions/deploy-pages/releases) - [Commits](https://github.com/actions/deploy-pages/compare/v1...v4) --- updated-dependencies: - dependency-name: actions/deploy-pages dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/pages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 92e3529..26f2421 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -41,4 +41,4 @@ jobs: with: path: _site - id: deployment - uses: actions/deploy-pages@v1 + uses: actions/deploy-pages@v4 From f618f0eaa4c0817673ca911bdcb097643e615a31 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:09:18 +0000 Subject: [PATCH 136/187] Bump actions/upload-pages-artifact from 1 to 3 Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 1 to 3. - [Release notes](https://github.com/actions/upload-pages-artifact/releases) - [Commits](https://github.com/actions/upload-pages-artifact/compare/v1...v3) --- updated-dependencies: - dependency-name: actions/upload-pages-artifact dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/pages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 26f2421..3f2eda8 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -37,7 +37,7 @@ jobs: cp -v _site/stackctl.1.html _site/index.html - uses: actions/configure-pages@v5 - - uses: actions/upload-pages-artifact@v1 + - uses: actions/upload-pages-artifact@v3 with: path: _site - id: deployment From d005c114268897692748f8fbd580b00c5f798573 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 21:55:07 +0000 Subject: [PATCH 137/187] Bump actions/checkout from 3 to 4 Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/pages.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 3f2eda8..1d992e6 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -22,7 +22,7 @@ jobs: steps: - run: echo "$HOME/.local/share/gem/ruby/3.0.0/bin" >>"$GITHUB_PATH" - run: gem install --user ronn-ng - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Generate HTML man-pages run: ronn --style toc,custom --html man/*.ronn diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ab13815..0559af5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -77,7 +77,7 @@ jobs: if: needs.tag.outputs.tag runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: freckle/stack-upload-action@v2 env: HACKAGE_API_KEY: ${{ secrets.HACKAGE_UPLOAD_API_KEY }} From 3ccac79ff320c8dfccd3d8cd6a2e9ea926cfd2ca Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Thu, 6 Feb 2025 15:49:09 -0500 Subject: [PATCH 138/187] Fix rubygems $PATH addition --- .github/workflows/pages.yml | 5 ++++- .github/workflows/release.yml | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 1d992e6..ced49d6 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -20,8 +20,11 @@ jobs: url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest steps: - - run: echo "$HOME/.local/share/gem/ruby/3.0.0/bin" >>"$GITHUB_PATH" - run: gem install --user ronn-ng + - run: | + for bin in "$HOME"/.local/share/gem/ruby/*/bin; do + echo "$bin" + done >>"$GITHUB_PATH" - uses: actions/checkout@v4 - name: Generate HTML man-pages diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0559af5..320a945 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -48,8 +48,11 @@ jobs: - uses: actions/checkout@v4 - uses: freckle/stack-cache-action@v2 - - run: echo "$HOME/.local/share/gem/ruby/3.0.0/bin" >>"$GITHUB_PATH" - run: gem install --user ronn-ng + - run: | + for bin in "$HOME"/.local/share/gem/ruby/*/bin; do + echo "$bin" + done >>"$GITHUB_PATH" - if: ${{ runner.os == 'macOS' }} run: brew install coreutils # need GNU install - run: | From 912b5736ada296e9b50c7bfbbcf572b5c36ee04c Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Thu, 6 Feb 2025 15:01:51 -0500 Subject: [PATCH 139/187] feat: implement semantic-release --- .github/release.yml | 11 ------ .github/workflows/ci.yml | 8 ++-- .github/workflows/release.yml | 74 ++++++++++++++--------------------- .releaserc.yaml | 17 ++++++++ 4 files changed, 49 insertions(+), 61 deletions(-) delete mode 100644 .github/release.yml create mode 100644 .releaserc.yaml diff --git a/.github/release.yml b/.github/release.yml deleted file mode 100644 index 4c9363d..0000000 --- a/.github/release.yml +++ /dev/null @@ -1,11 +0,0 @@ -changelog: - categories: - - title: Breaking Changes - labels: - - breaking-change - - title: Features - labels: - - enhancement - - title: Other Changes - labels: - - "*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6851f4d..c9fd844 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,8 +2,6 @@ name: CI on: pull_request: - push: - branches: main concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -33,10 +31,10 @@ jobs: stack-arguments: --stack-yaml ${{ matrix.stack-yaml }} lint: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: haskell/actions/hlint-setup@v2 - - uses: haskell/actions/hlint-run@v2 + - uses: haskell-actions/hlint-setup@v2 + - uses: haskell-actions/hlint-run@v2 with: fail-on: warning diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 320a945..364f865 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,34 +2,12 @@ name: Release on: push: - branches: main + branches: + - main + - rc/* jobs: - tag: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - id: tag - uses: freckle/haskell-tag-action@v1 - outputs: - tag: ${{ steps.tag.outputs.tag }} - - create-release: - needs: tag - if: needs.tag.outputs.tag - runs-on: ubuntu-latest - steps: - - id: create-release - uses: freckle/action-gh-release@v2 - with: - tag_name: ${{ needs.tag.outputs.tag }} - generate_release_notes: true - draft: true - outputs: - release_id: ${{ steps.create-release.outputs.id }} - - upload-assets: - needs: create-release + build: strategy: fail-fast: false matrix: @@ -58,29 +36,35 @@ jobs: - run: | make install.check # creates dist/stackctl.tar.gz cp -v dist/stackctl.tar.gz stackctl-${{ matrix.suffix }}.tar.gz - - uses: freckle/action-gh-release@v2 + - uses: actions/upload-artifact@v4 with: - id: ${{ needs.create-release.outputs.release_id }} - files: "*-${{ matrix.suffix }}.tar.gz" - fail_on_unmatched_files: true + name: ${{ matrix.os }}-binaries + path: "stackctl-*.tar.gz" + if-no-files-found: error - publish-release: - needs: - - create-release - - upload-assets + release: + needs: build runs-on: ubuntu-latest steps: - - uses: freckle/action-gh-release@v2 + - uses: actions/checkout@v4 with: - id: ${{ needs.create-release.outputs.release_id }} - draft: false + persist-credentials: false - upload-hackage: - needs: tag - if: needs.tag.outputs.tag - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: freckle/stack-upload-action@v2 + - uses: actions/download-artifact@v4 + + - id: token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ vars.FRECKLE_AUTOMATION_APP_ID }} + private-key: ${{ secrets.FRECKLE_AUTOMATION_PRIVATE_KEY }} + + - id: release + uses: cycjimmy/semantic-release-action@v4 + with: + extra_plugins: | + git+https://github.com/pbrisbin/semantic-release-stack-upload.git env: - HACKAGE_API_KEY: ${{ secrets.HACKAGE_UPLOAD_API_KEY }} + FORCE_COLOR: 1 + GITHUB_TOKEN: ${{ steps.token.outputs.token }} + HACKAGE_KEY: ${{ secrets.HACKAGE_UPLOAD_API_KEY }} + STACK_YAML: stack-lts-20.4.yaml diff --git a/.releaserc.yaml b/.releaserc.yaml new file mode 100644 index 0000000..10a3bae --- /dev/null +++ b/.releaserc.yaml @@ -0,0 +1,17 @@ +tagFormat: "v1.${version}" # PVP prefixed + +plugins: + - "@semantic-release/commit-analyzer" + - "@semantic-release/release-notes-generator" + - - "@semantic-release/github" + - assets: "*-binaries/stackctl-*.tar.gz" + successCommentCondition: false + failCommentCondition: false + - - "semantic-release-stack-upload" + - pvpBounds: lower + stripSuffix: true + +branches: + - main + - name: rc/* + prerelease: '${name.replace(/^rc\//, "rc-")}' From 1be040cc9992aa7558d692a0f10fe2abc944af63 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Thu, 6 Feb 2025 16:10:23 -0500 Subject: [PATCH 140/187] chore(release): Move off of stack-cache-action --- .github/workflows/release.yml | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 364f865..66e5e4e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,20 +19,15 @@ jobs: runs-on: ${{ matrix.os }} steps: - # stack was removed in macOS-14 which is now latest - # https://discourse.haskell.org/t/github-hosted-runner-for-macos-aarch64/8717/16 - if: ${{ runner.os == 'macOS' }} - run: curl -sSL https://get.haskellstack.org/ | sh - - - uses: actions/checkout@v4 - - uses: freckle/stack-cache-action@v2 + run: brew install coreutils # need GNU install - run: gem install --user ronn-ng - run: | for bin in "$HOME"/.local/share/gem/ruby/*/bin; do echo "$bin" done >>"$GITHUB_PATH" - - if: ${{ runner.os == 'macOS' }} - run: brew install coreutils # need GNU install + - uses: actions/checkout@v4 + - uses: freckle/stack-action@v5 - run: | make install.check # creates dist/stackctl.tar.gz cp -v dist/stackctl.tar.gz stackctl-${{ matrix.suffix }}.tar.gz From 3543d3e8c56f71fa78ae57112d183c3efebba375 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Thu, 6 Feb 2025 16:13:26 -0500 Subject: [PATCH 141/187] chore: document release steps in README --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 720e194..f9c9ac4 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,19 @@ Once installed, see: The man pages are also available [online](https://freckle.github.io/stackctl/), but contain documentation as of `main`, and not your installed version. +## Release + +To trigger a release in this project, merge a commit to `main` with a +conventionally-formatted commit message. In short, one that starts with: + +1. `fix:` to trigger a patch release, +1. `feat:` for minor, or +1. `feat!:` for major + +Conventional commits are not required generally for this project, though you're +free to always use them. They are only required when you want to trigger a +release. + ## Comparison to AWS CloudFormation Git Sync [AWS CloudFormation Git Sync][aws-git-sync] was recently released by AWS. It From 8b569ded14f68398973415bece72ce676024b595 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Mon, 10 Feb 2025 15:13:44 -0500 Subject: [PATCH 142/187] chore: remove accidental fit --- test/Stackctl/CancelHandlerSpec.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Stackctl/CancelHandlerSpec.hs b/test/Stackctl/CancelHandlerSpec.hs index 0ad5679..e32ea5e 100644 --- a/test/Stackctl/CancelHandlerSpec.hs +++ b/test/Stackctl/CancelHandlerSpec.hs @@ -10,7 +10,7 @@ import Test.Hspec spec :: Spec spec = do describe "with" $ do - fit "installs a handler for the duration of a block" $ example $ do + it "installs a handler for the duration of a block" $ example $ do done <- newEmptyMVar CancelHandler.install $ putMVar done () From f6a4b20f121599f725511986376b45669e0396e5 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 11 Feb 2025 09:56:14 -0500 Subject: [PATCH 143/187] chore(release): use PREPARE_IN_VERIFY This ensures that `package.yaml` is updated when we build, so that our use of `Paths_stackctl.version` to implement the `version` subcommand is accurate. NOTE: I'm temporarily testing this feature in a branch of the release plugin, but I plan to land it in `main` before merging. --- .github/workflows/release.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 66e5e4e..054bc05 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,6 +27,17 @@ jobs: echo "$bin" done >>"$GITHUB_PATH" - uses: actions/checkout@v4 + + - id: release + uses: cycjimmy/semantic-release-action@v4 + with: + dry_run: true + extra_plugins: | + git+https://github.com/pbrisbin/semantic-release-stack-upload.git#pb/prepare-in-verify + env: + FORCE_COLOR: 1 + PREPARE_IN_VERIFY: 1 + - uses: freckle/stack-action@v5 - run: | make install.check # creates dist/stackctl.tar.gz @@ -57,7 +68,7 @@ jobs: uses: cycjimmy/semantic-release-action@v4 with: extra_plugins: | - git+https://github.com/pbrisbin/semantic-release-stack-upload.git + git+https://github.com/pbrisbin/semantic-release-stack-upload.git#pb/prepare-in-verify env: FORCE_COLOR: 1 GITHUB_TOKEN: ${{ steps.token.outputs.token }} From f811916794c9e08fc07624acb907b8dbfc76a1d8 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Tue, 11 Feb 2025 10:01:31 -0500 Subject: [PATCH 144/187] chore(release): fix dryRun env --- .github/workflows/release.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 054bc05..90b6425 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,6 +38,10 @@ jobs: FORCE_COLOR: 1 PREPARE_IN_VERIFY: 1 + # These are unused, but needed for verify to succeed + GITHUB_TOKEN: ${{ github.token }} + HACKAGE_KEY: ${{ secrets.HACKAGE_UPLOAD_API_KEY }} + - uses: freckle/stack-action@v5 - run: | make install.check # creates dist/stackctl.tar.gz From 654aa659e92842f3b25678a637b9dfb8aa6c0cc5 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Wed, 12 Feb 2025 12:27:41 -0500 Subject: [PATCH 145/187] chore(release): fix plugin git reference I merged that branch once we had it all working, which deleted it, but forgot to update this. Womp. --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 90b6425..8722473 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,7 +33,7 @@ jobs: with: dry_run: true extra_plugins: | - git+https://github.com/pbrisbin/semantic-release-stack-upload.git#pb/prepare-in-verify + git+https://github.com/pbrisbin/semantic-release-stack-upload.git env: FORCE_COLOR: 1 PREPARE_IN_VERIFY: 1 @@ -72,7 +72,7 @@ jobs: uses: cycjimmy/semantic-release-action@v4 with: extra_plugins: | - git+https://github.com/pbrisbin/semantic-release-stack-upload.git#pb/prepare-in-verify + git+https://github.com/pbrisbin/semantic-release-stack-upload.git env: FORCE_COLOR: 1 GITHUB_TOKEN: ${{ steps.token.outputs.token }} From 06ed00bf328baa8b46dc041196bdb85766cee12c Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Fri, 14 Feb 2025 07:27:42 -0500 Subject: [PATCH 146/187] chore(style): update fourmolu.yaml for v0.17 I've kept some sorting options as default for now to avoid churn. We're also not totally sure we like them (such as sorting constraints and how it changes type-variable order). We can enable them as later commits if we want. --- fourmolu.yaml | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/fourmolu.yaml b/fourmolu.yaml index 9211e93..292304b 100644 --- a/fourmolu.yaml +++ b/fourmolu.yaml @@ -1,15 +1,30 @@ indentation: 2 -column-limit: 80 # ignored until v12 / ghc-9.6 +column-limit: 80 # needs v0.12 function-arrows: leading comma-style: leading # default import-export-style: leading +import-grouping: # needs v0.17 + - name: "Preludes" + rules: + - glob: Prelude + - glob: "**.Prelude" + - glob: RIO + - glob: Stackctl.Test.App + - name: "Everything else" + rules: + - match: all + priority: 100 indent-wheres: false # default record-brace-space: true newlines-between-decls: 1 # default haddock-style: single-line let-style: mixed in-style: left-align -single-constraint-parens: never # ignored until v12 / ghc-9.6 +single-constraint-parens: never # needs v0.12 +sort-constraints: false # default +sort-derived-classes: false # default +sort-derived-clauses: false # default +trailing-section-operators: false # needs v0.17 unicode: never # default respectful: true # default From 798ada3b95d0b1f551230038b28ff936426f3c4c Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Fri, 14 Feb 2025 07:37:30 -0500 Subject: [PATCH 147/187] chore(style): group prelude imports --- src/Stackctl/Version.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Stackctl/Version.hs b/src/Stackctl/Version.hs index b768081..f1871df 100644 --- a/src/Stackctl/Version.hs +++ b/src/Stackctl/Version.hs @@ -3,10 +3,10 @@ module Stackctl.Version ) where import Stackctl.Prelude +import Prelude (putStrLn) import Data.Version import qualified Paths_stackctl as Pkg -import Prelude (putStrLn) logVersion :: MonadIO m => m () logVersion = liftIO $ putStrLn $ ("Stackctl v" <>) $ showVersion Pkg.version From f08cb449b182b68509efdf9d14a4b9ac66efcd15 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Fri, 14 Feb 2025 07:37:10 -0500 Subject: [PATCH 148/187] chore(style): reformat with new fourmolu fixities Newer fourmolu understands more fixities, which means it is indenting a few more operators further than others. These could be configured around, but I think less configuration is better and am only leaving the current lens operator fixities we had already. --- app/Main.hs | 10 ++--- src/Stackctl/AWS/CloudFormation.hs | 52 +++++++++++++------------- src/Stackctl/AWS/Core.hs | 8 ++-- src/Stackctl/AWS/Lambda.hs | 10 ++--- src/Stackctl/AWS/Scope.hs | 32 +++++++--------- src/Stackctl/Action.hs | 8 ++-- src/Stackctl/Config.hs | 7 ++-- src/Stackctl/Config/RequiredVersion.hs | 6 +-- src/Stackctl/FilterOption.hs | 4 +- src/Stackctl/Options.hs | 10 ++--- src/Stackctl/Spec/Capture.hs | 5 +-- src/Stackctl/Spec/Cat.hs | 14 +++---- src/Stackctl/Spec/Changes/Format.hs | 45 +++++++++++----------- src/Stackctl/Spec/Deploy.hs | 6 +-- src/Stackctl/Spec/Discover.hs | 23 ++++++------ src/Stackctl/Spec/List.hs | 2 +- src/Stackctl/StackSpec.hs | 6 +-- src/Stackctl/StackSpecPath.hs | 14 +++---- src/Stackctl/StackSpecYaml.hs | 6 +-- test/Stackctl/AWS/EC2Spec.hs | 2 +- test/Stackctl/AWS/LambdaSpec.hs | 8 ++-- test/Stackctl/ConfigSpec.hs | 9 ++--- test/Stackctl/Test/App.hs | 3 +- 23 files changed, 137 insertions(+), 153 deletions(-) diff --git a/app/Main.hs b/app/Main.hs index cecba33..3c4b678 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -11,8 +11,8 @@ main :: IO () main = runSubcommand $ subcommand Commands.cat - <> subcommand Commands.capture - <> subcommand Commands.changes - <> subcommand Commands.deploy - <> subcommand Commands.list - <> subcommand Commands.version + <> subcommand Commands.capture + <> subcommand Commands.changes + <> subcommand Commands.deploy + <> subcommand Commands.list + <> subcommand Commands.version diff --git a/src/Stackctl/AWS/CloudFormation.hs b/src/Stackctl/AWS/CloudFormation.hs index 0e21dc0..728b52b 100644 --- a/src/Stackctl/AWS/CloudFormation.hs +++ b/src/Stackctl/AWS/CloudFormation.hs @@ -194,7 +194,7 @@ awsCloudFormationDescribeStackMaybe stackName = handling_ _ValidationError (pure Nothing) $ awsSilently -- don't log said 400 $ Just - <$> awsCloudFormationDescribeStack stackName + <$> awsCloudFormationDescribeStack stackName awsCloudFormationDescribeStackOutputs :: (MonadIO m, MonadAWS m) @@ -218,10 +218,10 @@ awsCloudFormationDescribeStackEvents stackName mLastId = do runConduit $ AWS.paginate req - .| mapC (fromMaybe [] . (^. describeStackEventsResponse_stackEvents)) - .| concatC - .| takeWhileC (\e -> Just (e ^. stackEvent_eventId) /= mLastId) - .| sinkList + .| mapC (fromMaybe [] . (^. describeStackEventsResponse_stackEvents)) + .| concatC + .| takeWhileC (\e -> Just (e ^. stackEvent_eventId) /= mLastId) + .| sinkList awsCloudFormationGetStackNamesMatching :: (MonadIO m, MonadAWS m) @@ -232,12 +232,12 @@ awsCloudFormationGetStackNamesMatching p = do runConduit $ AWS.paginate req - .| concatMapC (^. listStacksResponse_stackSummaries) - .| concatC - .| mapC (^. stackSummary_stackName) - .| filterC ((p `match`) . unpack) - .| mapC StackName - .| sinkList + .| concatMapC (^. listStacksResponse_stackSummaries) + .| concatC + .| mapC (^. stackSummary_stackName) + .| filterC ((p `match`) . unpack) + .| mapC StackName + .| sinkList awsCloudFormationGetMostRecentStackEventId :: (MonadIO m, MonadAWS m) @@ -258,9 +258,9 @@ awsCloudFormationGetMostRecentStackEventId stackName = do AWS.simple req $ pure - . getFirstEventId - . fromMaybe [] - . (^. describeStackEventsResponse_stackEvents) + . getFirstEventId + . fromMaybe [] + . (^. describeStackEventsResponse_stackEvents) awsCloudFormationDeleteStack :: (MonadIO m, MonadLogger m, MonadAWS m) @@ -314,7 +314,7 @@ awaitStack awaitStack waiter stackName = AWS.await waiter $ newDescribeStacks - & describeStacks_stackName ?~ unStackName stackName + & describeStacks_stackName ?~ unStackName stackName makeParameter :: Text -> Maybe Text -> Parameter makeParameter k v = @@ -424,7 +424,7 @@ awsCloudFormationCreateChangeSet stackName mStackDescription stackTemplate param logInfo $ "Creating changeset..." - :# ["name" .= name, "type" .= changeSetType] + :# ["name" .= name, "type" .= changeSetType] csId <- AWS.simple req (^. createChangeSetResponse_id) logDebug "Awaiting CREATE_COMPLETE" @@ -471,16 +471,16 @@ awsCloudFormationDeleteAllChangeSets stackName = do logInfo "Deleting all changesets" runConduit $ AWS.paginate (newListChangeSets $ unStackName stackName) - .| concatMapC - ( \resp -> fromMaybe [] $ do - ss <- resp ^. listChangeSetsResponse_summaries - pure $ mapMaybe Summary.changeSetId ss - ) - .| mapM_C - ( \csId -> do - logInfo $ "Enqueing delete" :# ["changeSetId" .= csId] - void $ AWS.send $ newDeleteChangeSet csId - ) + .| concatMapC + ( \resp -> fromMaybe [] $ do + ss <- resp ^. listChangeSetsResponse_summaries + pure $ mapMaybe Summary.changeSetId ss + ) + .| mapM_C + ( \csId -> do + logInfo $ "Enqueing delete" :# ["changeSetId" .= csId] + void $ AWS.send $ newDeleteChangeSet csId + ) -- | Did we abandoned this Stack's first ever ChangeSet? -- diff --git a/src/Stackctl/AWS/Core.hs b/src/Stackctl/AWS/Core.hs index dba4001..b0db1f1 100644 --- a/src/Stackctl/AWS/Core.hs +++ b/src/Stackctl/AWS/Core.hs @@ -160,10 +160,10 @@ handlingServiceError = handling _ServiceError $ \e -> do logError $ "Exiting due to AWS Service error" - :# [ "code" .= toText (e ^. serviceError_code) - , "message" .= fmap toText (e ^. serviceError_message) - , "requestId" .= fmap toText (e ^. serviceError_requestId) - ] + :# [ "code" .= toText (e ^. serviceError_code) + , "message" .= fmap toText (e ^. serviceError_message) + , "requestId" .= fmap toText (e ^. serviceError_requestId) + ] exitFailure formatServiceError :: ServiceError -> Text diff --git a/src/Stackctl/AWS/Lambda.hs b/src/Stackctl/AWS/Lambda.hs index 61684ae..c0c9fba 100644 --- a/src/Stackctl/AWS/Lambda.hs +++ b/src/Stackctl/AWS/Lambda.hs @@ -93,11 +93,11 @@ awsLambdaInvoke name payload = do logDebug $ "Function result" - :# [ "name" .= name - , "status" .= status - , "error" .= mError - , "functionError" .= mFunctionError - ] + :# [ "name" .= name + , "status" .= status + , "error" .= mError + , "functionError" .= mFunctionError + ] pure $ if diff --git a/src/Stackctl/AWS/Scope.hs b/src/Stackctl/AWS/Scope.hs index 3d3f72e..909ec8d 100644 --- a/src/Stackctl/AWS/Scope.hs +++ b/src/Stackctl/AWS/Scope.hs @@ -26,20 +26,14 @@ awsScopeSpecPatterns :: AwsScope -> [Pattern] awsScopeSpecPatterns AwsScope {..} = [ compile $ "stacks" - unpack (unAccountId awsAccountId) - <> ".*" - unpack (fromRegion awsRegion) - <> "**" - "*" - <.> "yaml" + unpack (unAccountId awsAccountId) <> ".*" + unpack (fromRegion awsRegion) <> "**" + "*" <.> "yaml" , compile $ "stacks" - "*." - <> unpack (unAccountId awsAccountId) - unpack (fromRegion awsRegion) - <> "**" - "*" - <.> "yaml" + "*." <> unpack (unAccountId awsAccountId) + unpack (fromRegion awsRegion) <> "**" + "*" <.> "yaml" ] awsScopeSpecStackName :: AwsScope -> FilePath -> Maybe StackName @@ -50,13 +44,13 @@ awsScopeSpecStackName scope path = do -- pretty fast and loose with the "parsing" step pure $ path -- stacks/account/region/x/y.yaml - & splitPath -- [stacks/, account/, region/, x/, y.yaml] - & drop 3 -- [x, y.yaml] - & joinPath -- x/y.yaml - & dropExtension -- x/y - & pack - & T.replace "/" "-" -- x-y - & StackName + & splitPath -- [stacks/, account/, region/, x/, y.yaml] + & drop 3 -- [x, y.yaml] + & joinPath -- x/y.yaml + & dropExtension -- x/y + & pack + & T.replace "/" "-" -- x-y + & StackName class HasAwsScope env where awsScopeL :: Lens' env AwsScope diff --git a/src/Stackctl/Action.hs b/src/Stackctl/Action.hs index ffc496a..ed05c0a 100644 --- a/src/Stackctl/Action.hs +++ b/src/Stackctl/Action.hs @@ -128,10 +128,10 @@ runAction stackName Action {on, run} = do Nothing -> do logError $ "Output not found" - :# [ "stackName" .= stackName - , "desiredOutput" .= outputName - , "availableOutputs" .= map (^. output_outputKey) outputs - ] + :# [ "stackName" .= stackName + , "desiredOutput" .= outputName + , "availableOutputs" .= map (^. output_outputKey) outputs + ] throwIO NoSuchOutput Just name -> invoke name InvokeLambdaByName name -> invoke name diff --git a/src/Stackctl/Config.hs b/src/Stackctl/Config.hs index 9cf2148..2092179 100644 --- a/src/Stackctl/Config.hs +++ b/src/Stackctl/Config.hs @@ -75,9 +75,10 @@ loadConfigOrExit = either die pure =<< loadConfig loadConfig :: MonadIO m => m (Either ConfigError Config) loadConfig = - runExceptT $ getConfigFile >>= \case - Nothing -> pure emptyConfig - Just cf -> loadConfigFrom cf + runExceptT + $ getConfigFile >>= \case + Nothing -> pure emptyConfig + Just cf -> loadConfigFrom cf loadConfigFrom :: (MonadIO m, MonadError ConfigError m) => FilePath -> m Config loadConfigFrom path = loadConfigFromBytes =<< liftIO (readFileBinary path) diff --git a/src/Stackctl/Config/RequiredVersion.hs b/src/Stackctl/Config/RequiredVersion.hs index 56e119e..842f2f3 100644 --- a/src/Stackctl/Config/RequiredVersion.hs +++ b/src/Stackctl/Config/RequiredVersion.hs @@ -54,7 +54,7 @@ requiredVersionFromText = fromWords . T.words ws -> Left $ show (unpack $ T.unwords ws) - <> " did not parse as optional operator and version string" + <> " did not parse as optional operator and version string" parseRequiredVersion :: Text -> Text -> Either String RequiredVersion parseRequiredVersion op w = RequiredVersion <$> parseOp op <*> parseVersion w @@ -71,8 +71,8 @@ requiredVersionFromText = fromWords . T.words op -> Left $ "Invalid comparison operator (" - <> unpack op - <> "), may only be =, <, <=, >, >=, or =~" + <> unpack op + <> "), may only be =, <, <=, >, >=, or =~" parseVersion :: Text -> Either String Version parseVersion t = diff --git a/src/Stackctl/FilterOption.hs b/src/Stackctl/FilterOption.hs index 0310e50..90ae154 100644 --- a/src/Stackctl/FilterOption.hs +++ b/src/Stackctl/FilterOption.hs @@ -44,8 +44,8 @@ envFilterOption items = var "FILTERS" <|> var "FILTER" Env.var (first Env.UnreadError . readFilterOption) name $ Env.help $ "Filter " - <> items - <> " by patterns" + <> items + <> " by patterns" filterOption :: String -> Parser FilterOption filterOption items = diff --git a/src/Stackctl/Options.hs b/src/Stackctl/Options.hs index ba3f927..6f472bc 100644 --- a/src/Stackctl/Options.hs +++ b/src/Stackctl/Options.hs @@ -55,11 +55,11 @@ envParser :: Env.Parser Env.Error Options envParser = Env.prefixed "STACKCTL_" $ Options - <$> optional envDirectoryOption - <*> optional (envFilterOption "specifications") - <*> pure mempty -- use LOG_COLOR - <*> pure mempty -- use LOG_LEVEL - <*> optional envAutoSSOOption + <$> optional envDirectoryOption + <*> optional (envFilterOption "specifications") + <*> pure mempty -- use LOG_COLOR + <*> pure mempty -- use LOG_LEVEL + <*> optional envAutoSSOOption -- brittany-disable-next-binding diff --git a/src/Stackctl/Spec/Capture.hs b/src/Stackctl/Spec/Capture.hs index 2332edd..d71d790 100644 --- a/src/Stackctl/Spec/Capture.hs +++ b/src/Stackctl/Spec/Capture.hs @@ -132,9 +132,8 @@ runCapture CaptureOptions {..} = do case results of [] -> do logError - $ "No Active Stacks match " - <> pack (decompile scoStackName) - :# [] + $ "No Active Stacks match " <> pack (decompile scoStackName) + :# [] exitFailure [stackName] -> do stack <- awsCloudFormationDescribeStack stackName diff --git a/src/Stackctl/Spec/Cat.hs b/src/Stackctl/Spec/Cat.hs index bf62dd3..3ef1ffa 100644 --- a/src/Stackctl/Spec/Cat.hs +++ b/src/Stackctl/Spec/Cat.hs @@ -138,11 +138,11 @@ prettyPrintStackSpecYaml Colors {..} name StackSpecYaml {..} = kvs <- f <$> mA pure $ [cyan label <> ":"] - <> map - ( \(k, mV) -> - " " <> cyan k <> ":" <> maybe "" (\v -> " " <> green v) mV - ) - kvs + <> 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) @@ -186,8 +186,8 @@ prettyPrintTemplate Colors {..} val = displayObjectProperty = displayPropertyWith @(HashMap Text Value) $ map ((" - " <>) . green) - . sort - . HashMap.keys + . sort + . HashMap.keys displayPropertyWith :: (FromJSON a, ToJSON a) => (a -> [Text]) -> Text -> [Text] diff --git a/src/Stackctl/Spec/Changes/Format.hs b/src/Stackctl/Spec/Changes/Format.hs index ddb5291..359a27c 100644 --- a/src/Stackctl/Spec/Changes/Format.hs +++ b/src/Stackctl/Spec/Changes/Format.hs @@ -76,10 +76,11 @@ formatTTY colors@Colors {..} name mChangeSet = case (mChangeSet, rChanges) of (Nothing, _) -> "No changes for " <> name (_, Nothing) -> "Metadata only changes (e.g. Tags or Outputs)" (_, Just rcs) -> - ("\n" <>) $ (<> "\n") $ mconcat $ ("Changes for " <> cyan name <> ":") - : map - (("\n " <>) . formatResourceChange) - (NE.toList rcs) + ("\n" <>) + $ (<> "\n") + $ mconcat + $ ("Changes for " <> cyan name <> ":") + : map (("\n " <>) . formatResourceChange) (NE.toList rcs) where rChanges = do cs <- mChangeSet @@ -149,20 +150,20 @@ commentBody omitFull cs rcs = , "\n| Action | Logical Id | Physical Id | Type | Replacement | Scope | Details |" , "\n| --- | --- | --- | --- | --- | --- | --- |" ] - <> map commentTableRow (NE.toList rcs) - <> case omitFull of - OmitFull -> [] - IncludeFull -> - [ "\n" - , "\n
            " - , "\nFull changes" - , "\n" - , "\n```json" - , "\n" <> changeSetJSON cs - , "\n```" - , "\n" - , "\n
            " - ] + <> map commentTableRow (NE.toList rcs) + <> case omitFull of + OmitFull -> [] + IncludeFull -> + [ "\n" + , "\n
            " + , "\nFull changes" + , "\n" + , "\n```json" + , "\n" <> changeSetJSON cs + , "\n```" + , "\n" + , "\n
            " + ] commentTableRow :: ResourceChange -> Text commentTableRow ResourceChange' {..} = @@ -194,10 +195,10 @@ formatDetail Colors {..} ResourceChangeDetail' {..} = do pure $ toText c - <> maybe "" ((" in " <>) . toText) attr - <> maybe "" (\x -> " (" <> magenta (toText x) <> ")") n - <> maybe "" ((", recreation " <>) . formatRR) rr - <> maybe "" ((", caused by " <>) . toText) causingEntity + <> maybe "" ((" in " <>) . toText) attr + <> maybe "" (\x -> " (" <> magenta (toText x) <> ")") n + <> maybe "" ((", recreation " <>) . formatRR) rr + <> maybe "" ((", caused by " <>) . toText) causingEntity where formatRR = \case x@RequiresRecreation_Always -> red (toText x) diff --git a/src/Stackctl/Spec/Deploy.hs b/src/Stackctl/Spec/Deploy.hs index 08295b6..d8c2913 100644 --- a/src/Stackctl/Spec/Deploy.hs +++ b/src/Stackctl/Spec/Deploy.hs @@ -266,8 +266,7 @@ formatStackEvent Colors {..} e = do timestamp <- liftIO $ formatTime defaultTimeLocale "%F %T %Z" - <$> utcToLocalZonedTime - (e ^. stackEvent_timestamp) + <$> utcToLocalZonedTime (e ^. stackEvent_timestamp) pure $ mconcat @@ -276,8 +275,7 @@ formatStackEvent Colors {..} e = do , maybe "" colorStatus $ e ^. stackEvent_resourceStatus , maybe "" (magenta . (" " <>)) $ e ^. stackEvent_logicalResourceId , maybe "" ((\x -> " (" <> x <> ")") . T.strip) - $ e - ^. stackEvent_resourceStatusReason + $ e ^. stackEvent_resourceStatusReason ] where colorStatus = \case diff --git a/src/Stackctl/Spec/Discover.hs b/src/Stackctl/Spec/Discover.hs index a0b6e2a..bcec218 100644 --- a/src/Stackctl/Spec/Discover.hs +++ b/src/Stackctl/Spec/Discover.hs @@ -105,9 +105,9 @@ checkForDuplicateStackNames = logError $ "Multiple specifications produced the same Stack name" - :# [ "name" .= stackSpecPathStackName (NE.head specPaths) - , "paths" .= collidingPaths - ] + :# [ "name" .= stackSpecPathStackName (NE.head specPaths) + , "paths" .= collidingPaths + ] exitFailure @@ -130,18 +130,17 @@ checkForUnknownDepends known spec = for_ depends $ \depend -> do let (nearest, _distance) = NE.minimumBy1 (comparing snd) - $ (id &&& getDistance depend) - <$> known + $ (id &&& getDistance depend) <$> known logWarn $ "Stack lists dependency that does not exist" - :# [ "dependency" - .= ( unStackName (stackSpecStackName spec) - <> " -> " - <> unStackName depend - ) - , "hint" .= ("Did you mean " <> unStackName nearest <> "?") - ] + :# [ "dependency" + .= ( unStackName (stackSpecStackName spec) + <> " -> " + <> unStackName depend + ) + , "hint" .= ("Did you mean " <> unStackName nearest <> "?") + ] getDistance = levenshtein `on` unStackName diff --git a/src/Stackctl/Spec/List.hs b/src/Stackctl/Spec/List.hs index 90aecac..097717a 100644 --- a/src/Stackctl/Spec/List.hs +++ b/src/Stackctl/Spec/List.hs @@ -79,7 +79,7 @@ runList ListOptions {..} = do when loLegend $ pushLoggerLn $ "\nLegend:\n " - <> T.intercalate ", " (map legendItem [minBound .. maxBound]) + <> T.intercalate ", " (map legendItem [minBound .. maxBound]) data Indicator = Deployed diff --git a/src/Stackctl/StackSpec.hs b/src/Stackctl/StackSpec.hs index 1ea2f17..e2e0300 100644 --- a/src/Stackctl/StackSpec.hs +++ b/src/Stackctl/StackSpec.hs @@ -81,8 +81,7 @@ stackSpecTemplate :: StackSpec -> StackTemplate stackSpecTemplate spec = StackTemplate $ FilePath.normalise - $ ssSpecRoot spec - stackSpecTemplateFile spec + $ ssSpecRoot spec stackSpecTemplateFile spec stackSpecParameters :: StackSpec -> [Parameter] stackSpecParameters = @@ -197,8 +196,7 @@ createChangeSet spec parameters tags = (stackSpecStackName spec) (stackSpecStackDescription spec) (stackSpecTemplate spec) - ( nubOrdOn (^. parameter_parameterKey) $ parameters <> stackSpecParameters spec - ) + (nubOrdOn (^. parameter_parameterKey) $ parameters <> stackSpecParameters spec) (stackSpecCapabilities spec) (nubOrdOn (^. tag_key) $ tags <> stackSpecTags spec) diff --git a/src/Stackctl/StackSpecPath.hs b/src/Stackctl/StackSpecPath.hs index a5ce673..8500b61 100644 --- a/src/Stackctl/StackSpecPath.hs +++ b/src/Stackctl/StackSpecPath.hs @@ -83,16 +83,16 @@ stackSpecPathFromFilePath awsScope@AwsScope {..} path = unless (pathAccountId == awsAccountId) $ Left $ "Unexpected account: " - <> unpack (unAccountId pathAccountId) - <> " != " - <> unpack (unAccountId awsAccountId) + <> unpack (unAccountId pathAccountId) + <> " != " + <> unpack (unAccountId awsAccountId) unless (unpack (fromRegion awsRegion) == pathRegion) $ Left $ "Unexpected region: " - <> pathRegion - <> " != " - <> unpack (fromRegion awsRegion) + <> pathRegion + <> " != " + <> unpack (fromRegion awsRegion) stackName <- maybe (Left "Must end in .yaml") (Right . StackName) @@ -117,6 +117,6 @@ parseAccountPath path = case second (T.drop 1) $ T.breakOn "." $ pack path of _ -> Left $ "Path matches neither {account-id}.{account-name}, nor {account-name}.{account-id}: " - <> path + <> path where isAccountId x = T.length x == 12 && T.all isDigit x diff --git a/src/Stackctl/StackSpecYaml.hs b/src/Stackctl/StackSpecYaml.hs index 7ae052c..7cb0e60 100644 --- a/src/Stackctl/StackSpecYaml.hs +++ b/src/Stackctl/StackSpecYaml.hs @@ -85,8 +85,7 @@ instance Semigroup ParametersYaml where $ KeyMap.toList $ KeyMap.fromListWith (<>) $ map (pyKey &&& pyValue) - $ bs -- flipped to make sure Last-wins - <> as + $ bs <> as -- flipped to make sure Last-wins instance FromJSON ParametersYaml where parseJSON = \case @@ -215,8 +214,7 @@ instance Semigroup TagsYaml where $ HashMap.toList $ HashMap.fromList $ map (toPair . unTagYaml) - $ as - <> bs + $ as <> bs where toPair :: Tag -> (Text, Text) toPair = (^. tag_key) &&& (^. tag_value) diff --git a/test/Stackctl/AWS/EC2Spec.hs b/test/Stackctl/AWS/EC2Spec.hs index df71979..4f1d8a0 100644 --- a/test/Stackctl/AWS/EC2Spec.hs +++ b/test/Stackctl/AWS/EC2Spec.hs @@ -23,7 +23,7 @@ spec = do $ Right $ newDescribeAvailabilityZonesResponse 200 & describeAvailabilityZonesResponse_availabilityZones - ?~ zones + ?~ zones withMatcher matcher awsEc2DescribeFirstAvailabilityZoneRegionName `shouldReturn` "us-east-1" diff --git a/test/Stackctl/AWS/LambdaSpec.hs b/test/Stackctl/AWS/LambdaSpec.hs index bfb3b92..6226718 100644 --- a/test/Stackctl/AWS/LambdaSpec.hs +++ b/test/Stackctl/AWS/LambdaSpec.hs @@ -33,18 +33,16 @@ spec = do [ SendMatcher (isInvocation "lambda-1") $ Right $ newInvokeResponse 200 - & invokeResponse_payload - ?~ "" + & invokeResponse_payload ?~ "" , SendMatcher (isInvocation "lambda-2") $ Right $ newInvokeResponse 200 - & invokeResponse_payload - ?~ BSL.toStrict (encode lambdaError) + & invokeResponse_payload ?~ BSL.toStrict (encode lambdaError) , SendMatcher (isInvocation "lambda-3") $ Right $ newInvokeResponse 500 & (invokeResponse_payload ?~ "") - . (invokeResponse_functionError ?~ "") + . (invokeResponse_functionError ?~ "") ] withMatchers matchers $ do diff --git a/test/Stackctl/ConfigSpec.hs b/test/Stackctl/ConfigSpec.hs index 971c25b..3d25d07 100644 --- a/test/Stackctl/ConfigSpec.hs +++ b/test/Stackctl/ConfigSpec.hs @@ -32,8 +32,7 @@ spec = do case result of Left err -> do expectationFailure - $ "Expected to load a Config, got error: " - <> show err + $ "Expected to load a Config, got error: " <> show err Right config -> do configParameters config `shouldBe` Just (toParametersYaml [("Some", Just "Parameter")]) @@ -56,9 +55,9 @@ spec = do Right config = loadConfigFromBytes $ "defaults:" - <> "\n tags:" - <> "\n From: Defaults" - <> "\n Keep: \"You?\"" + <> "\n tags:" + <> "\n From: Defaults" + <> "\n Keep: \"You?\"" Just tags = ssyTags (applyConfig config specYaml) diff --git a/test/Stackctl/Test/App.hs b/test/Stackctl/Test/App.hs index afa45c9..3e4f436 100644 --- a/test/Stackctl/Test/App.hs +++ b/test/Stackctl/Test/App.hs @@ -92,5 +92,4 @@ testAppStackFilePath base = "stacks" "0123456789.test" "us-east-1" - unpack base - <.> "yaml" + unpack base <.> "yaml" From eae06ea0ae3a95f306855be3f6acadee539e81d2 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Fri, 14 Feb 2025 09:13:21 -0500 Subject: [PATCH 149/187] chore(style): use fourmolu v0.17 with restyled --- .restyled.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.restyled.yaml b/.restyled.yaml index 20a1cec..c25b270 100644 --- a/.restyled.yaml +++ b/.restyled.yaml @@ -4,7 +4,7 @@ restylers: enabled: false - fourmolu: image: - tag: v0.14.1.0 + tag: v0.17.0.0 - stylish-haskell: enabled: false - prettier-markdown: From 3ec55aecfa49637c1f1609222ed4f6384ef8e59a Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Fri, 14 Feb 2025 09:46:58 -0500 Subject: [PATCH 150/187] chore(ci): add restyled workflow --- .github/workflows/restyled.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/restyled.yml diff --git a/.github/workflows/restyled.yml b/.github/workflows/restyled.yml new file mode 100644 index 0000000..72720ab --- /dev/null +++ b/.github/workflows/restyled.yml @@ -0,0 +1,22 @@ +name: Restyled + +on: + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + pull-requests: write + +jobs: + restyled: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: restyled-io/actions/setup@v4 + - uses: restyled-io/actions/run@v4 + with: + suggestions: true From df0237674ae41becbe38777434fa11c8cc9fb812 Mon Sep 17 00:00:00 2001 From: Pat Brisbin Date: Thu, 20 Feb 2025 08:46:30 -0500 Subject: [PATCH 151/187] chore: change CHANGELOG to link to Releases Releases have the same or better information, and is automatically updated as part of semantic-release without any extra configuration. --- CHANGELOG.md | 243 +-------------------------------------------------- 1 file changed, 1 insertion(+), 242 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05c1df4..f63b56a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,242 +1 @@ -## [_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) - -- Add `withAssumedRole`, deprecate `assumeRole` - -## [v1.7.0.0](https://github.com/freckle/stackctl/compare/v1.6.1.2...v1.7.0.0) - -- Retain numeric parameter types, and add boolean parameter handling, in our own - yaml generation - - This required changing the `Generate` to use `ParametersYaml` instead of - `[Parameter]`, and ultimately led us to removing it, hence the major version - bump. - -## [v1.6.1.2](https://github.com/freckle/stackctl/compare/v1.6.1.1...v1.6.1.2) - -- Require Blammo-1.2.2.3 - -## [v1.6.1.1](https://github.com/freckle/stackctl/compare/v1.6.1.0...v1.6.1.1) - -- Fix: finding removed stacks now respects `STACKCTL_DIRECTORY` - -## [v1.6.1.0](https://github.com/freckle/stackctl/compare/v1.6.0.0...v1.6.1.0) - -- Add `Ord` instance on `RequiredVersion` and `RequiredVersionOp` - -## [v1.6.0.0](https://github.com/freckle/stackctl/compare/v1.5.0.1...v1.6.0.0) - -- Re-implement `Stackctl.AWS` with `amazonka-mtl`. - -_No CLI or behavior changes._ - -## [v1.5.0.1](https://github.com/freckle/stackctl/compare/v1.5.0.0...v1.5.0.1) - -- Handle missing-or-empty specs directory more explicitly -- Add warning for `Depends` pointing to non-existent spec -- Fix formatting of required version in warning message - -## [v1.5.0.0](https://github.com/freckle/stackctl/compare/v1.4.4.0...v1.5.0.0) - -Breaking changes: - -- Don't require a name argument to the `awsSimple` function - -New features: - -- Add `Exec` and `Shell` features in `actions[].run` -- Support lists in `actions[].run` (single items still work) -- Add more granular status indicators in `stack-ls(1)` output, print a legend of - these indicators as a footer (disable with `--no-legend`) - -Fixes: - -- Fix for redundant change-set creation errors in logging output -- Fix globbing bug in auto-expansion of `--filter` arguments - -## [v1.4.4.0](https://github.com/freckle/stackctl/compare/v1.4.3.0...v1.4.4.0) - -- Add `awsSilently` - -## [v1.4.3.0](https://github.com/freckle/stackctl/compare/v1.4.2.2...v1.4.3.0) - -- Add `awsWithAuth` -- Add `forEachSpec_` - -## [v1.4.2.2](https://github.com/freckle/stackctl/compare/v1.4.2.1...v1.4.2.2) - -- Use `amazonka-2.0` :tada: -- Finalize update to `UnliftIO.Exception.Lens` -- Re-export upstreamed `Blammo.Logging.Colors` - -## [v1.4.2.1](https://github.com/freckle/stackctl/compare/v1.4.2.0...v1.4.2.1) - -No changes. Bumped to trigger release workflow. - -## [v1.4.2.0](https://github.com/freckle/stackctl/compare/v1.4.0.1...v1.4.2.0) - -- Add `stackctl-ls` for listing stacks and their deployed status -- Add `--auto-sso` option for automating `aws sso login` when required - -## [v1.4.0.1](https://github.com/freckle/stackctl/compare/v1.4.0.0...v1.4.0.1) - -- Document and read a consistently-named `STACKCTL_FILTER` for `--filter`. For - now, the old and incorrect `STACKCTL_FILTERS` will still work. - -## [v1.4.0.0](https://github.com/freckle/stackctl/compare/v1.3.0.2...v1.4.0.0) - -- Add `awsAssumeRole` for running an action as an assumed role -- Refactor `Generate` interface to better support generating stacks with - pre-existing templates - -## [v1.3.0.2](https://github.com/freckle/stackctl/compare/v1.3.0.1...v1.3.0.2) - -- Adjust timeout when invoking Lambdas to allow up to Lambda's own execution - timeout (15 minutes). - -## [v1.3.0.1](https://github.com/freckle/stackctl/compare/v1.3.0.0...v1.3.0.1) - -- Fix bug where `LOG_COLOR` was never respected -- Also accept `"required_version: == "` -- Add `Eq`, `ToJSON` instance on `RequiredVersion` - -## [v1.3.0.0](https://github.com/freckle/stackctl/compare/v1.2.0.1...v1.3.0.0) - -- Fix it so commands like `version` don't need a valid AWS environment - - This changes the `Subcommand` interface and so is a major version update for - the purposes of those using Stackctl as a library. - -## [v1.2.0.0](https://github.com/freckle/stackctl/compare/v1.1.3.1...v1.2.0.0) - -- Use more specific types in `Has{Directory,Filter,Color}Option` -- Add environment variable configuration for `STACKCTL_{DIRECTORY,FILTERS}` - -## [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) - -- Fix JSON formatting bugs in generating specification - -## [v1.1.3.0](https://github.com/freckle/stackctl/compare/v1.1.2.2...v1.1.3.0) - -- Repository-local configuration - - See https://github.com/freckle/stackctl/commit/564678203fe70b5c4c46c655dd3daeaafb6de9e0 - -- Don't duplicate re-used templates in `stackctl-cat` -- Improve `--filter` - - - Match against stack name and template, in addition to spec path. - - Automatically prepend `**/` (unless there is already a leading wildcard) and - append `{/*,.yaml,.json}` (unless there is already a trailing wildcard or - extension). - - In general, this aims to make `--filter` match more things more intuitively - for operators, but still match exactly in programmatic use-cases. - -- Various documentation improvements -- Support more natural `{Key}: {Value}` syntax in `Parameters` and `Tags` -- Fix bug where we may generate an `{}` element in `Parameters` - -## [v1.1.2.2](https://github.com/freckle/stackctl/compare/v1.1.2.1...v1.1.2.2) - -- Add support for Stack descriptions - -## [v1.1.2.1](https://github.com/freckle/stackctl/compare/v1.1.2.0...v1.1.2.1) - -- Build with LTS-20.4 / GHC 9.2 - -## [v1.1.2.0](https://github.com/freckle/stackctl/compare/v1.1.1.1...v1.1.2.0) - -- Fix incorrect ordering of log-messages by setting `LOG_CONCURRENCY=1` -- Fix potential coloring of changes being redirected to a file -- Make `PATH` optional (again) in `stackctl changes` -- Add `--no-flip` to `stackctl capture` - -## [v1.1.1.1](https://github.com/freckle/stackctl/compare/v1.1.1.0...v1.1.1.1) - -- Trigger release - -## [v1.1.1.0](https://github.com/freckle/stackctl/compare/v1.1.0.5...v1.1.1.0) - -- Add `--parameter` to `changes` and `deploy` -- Sort changes by causing-before-caused - -## [v1.1.0.5](https://github.com/freckle/stackctl/compare/v1.1.0.4...v1.1.0.5) - -- Trigger release workflow - -## [v1.1.0.4](https://github.com/freckle/stackctl/compare/v1.1.0.3...v1.1.0.4) - -- Fix bug where only the last spec in a multi-spec case had its changes present - in the output file generated by `changes`. - -## [v1.1.0.3](https://github.com/freckle/stackctl/compare/v1.1.0.2...v1.1.0.3) - -- Require Blammo-1.1.1.0 - -## [v1.1.0.2](https://github.com/freckle/stackctl/compare/v1.1.0.1...v1.1.0.2) - -- Log responses from `awsLambdaInvoke` when running actions -- Clarify discovery logging -- Add install script - -## [v1.1.0.1](https://github.com/freckle/stackctl/compare/v1.1.0.0...v1.1.0.1) - -- Update to `cfn-flip-0.1.0.3` - -## [v1.1.0.0](https://github.com/freckle/stackctl/compare/v1.0.2.0...v1.1.0.0) - -- Fix interleaved or out-of-order output bugs by streaming deployment events - through the Logger instead of directly to `stdout` -- Logging goes to `stdout` by default (`LOG_DESTINATION` can still be used) -- The `changes` subcommand now requires a `PATH` argument - -## [v1.0.2.0](https://github.com/freckle/stackctl/compare/v1.0.1.2...v1.0.2.0) - -- Add `Stackctl.Action` - - Support for taking actions during Stack management, currently we support - invoking a lambda post-deployment. In the future, we can add more, such as - running local pre-deploy validation or preparation scripts. - -- Add `awsCloudFormationDescribeStackOutputs` - -## [v1.0.1.2](https://github.com/freckle/stackctl/compare/v1.0.1.1...v1.0.1.2) - -- Always flush log messages before our own output - -## [v1.0.1.1](https://github.com/freckle/stackctl/compare/v1.0.1.0...v1.0.1.1) - -- Respect `LOG_DESTINATION` (the default remains `stderr`) - -## [v1.0.1.0](https://github.com/freckle/stackctl/compare/v1.0.0.2...v1.0.1.0) - -- Support reading CloudGenesis specifications - - - Accept account paths like `id.name` or `name.id` - - Read `Parameters` as `Parameter{Key,Value}` or `{Name,Value}` - - This allows us to work with specifications directories originally implemented - for, and potentially still used with, the CloudGenesis tooling. - -## [v1.0.0.2](https://github.com/freckle/stackctl/compare/v1.0.0.1...v1.0.0.2) - -- Fix tailing all events to read most recent, causing Throttling errors - -## [v1.0.0.1](https://github.com/freckle/stackctl/compare/v1.0.0.0...v1.0.0.1) - -- Fix non-portable paths issue in OSX executable build - -## [v1.0.0.0](https://github.com/freckle/stackctl/tree/v1.0.0.0) - -First release +See https://github.com/freckle/stackctl/releases From 983882cd429d90dcc0c1daf1d8165a5f97a83f56 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Thu, 6 Feb 2025 14:54:30 -0500 Subject: [PATCH 152/187] fix(deps): update resolver and Blammo Refactoring the CLI to use `WithLogger` should fix a bug with the logger not being flushed correctly in the presence of exceptions, and error logging being lost. --- package.yaml | 1 + src/Stackctl/AWS/CloudFormation.hs | 2 + src/Stackctl/AWS/Core.hs | 2 - src/Stackctl/AWS/Orphans.hs | 3 + src/Stackctl/CLI.hs | 63 ++++++++-------- src/Stackctl/Prelude.hs | 2 + src/Stackctl/StackSpecYaml.hs | 8 +- src/Stackctl/VerboseOption.hs | 1 + stack-lts-20.4.yaml | 22 ------ stack-lts-20.4.yaml.lock | 117 ----------------------------- stack.yaml | 15 +++- stack.yaml.lock | 109 ++++++++++++++++++++++++--- stackctl.cabal | 6 +- test/Stackctl/Test/App.hs | 1 + 14 files changed, 161 insertions(+), 191 deletions(-) delete mode 100644 stack-lts-20.4.yaml delete mode 100644 stack-lts-20.4.yaml.lock diff --git a/package.yaml b/package.yaml index 4661368..7cd0871 100644 --- a/package.yaml +++ b/package.yaml @@ -24,6 +24,7 @@ ghc-options: - -Wno-missing-import-lists - -Wno-missing-kind-signatures - -Wno-missing-local-signatures + - -Wno-missing-role-annotations - -Wno-missing-safe-haskell-mode - -Wno-prepositive-qualified-module - -Wno-unsafe diff --git a/src/Stackctl/AWS/CloudFormation.hs b/src/Stackctl/AWS/CloudFormation.hs index 728b52b..bb2e2c7 100644 --- a/src/Stackctl/AWS/CloudFormation.hs +++ b/src/Stackctl/AWS/CloudFormation.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE DuplicateRecordFields #-} + module Stackctl.AWS.CloudFormation ( Stack (..) , stack_stackName diff --git a/src/Stackctl/AWS/Core.hs b/src/Stackctl/AWS/Core.hs index b0db1f1..eff7c88 100644 --- a/src/Stackctl/AWS/Core.hs +++ b/src/Stackctl/AWS/Core.hs @@ -77,8 +77,6 @@ simple , MonadIO m , MonadAWS m , AWSRequest a - , Typeable a - , Typeable (AWSResponse a) ) => a -> (AWSResponse a -> Maybe b) diff --git a/src/Stackctl/AWS/Orphans.hs b/src/Stackctl/AWS/Orphans.hs index 00658c0..615cbfe 100644 --- a/src/Stackctl/AWS/Orphans.hs +++ b/src/Stackctl/AWS/Orphans.hs @@ -14,6 +14,9 @@ import Amazonka.CloudFormation.Types import Data.Aeson import GHC.Generics (Rep) +-- TODO: upstream +deriving newtype instance MonadUnliftIO m => MonadUnliftIO (WithLogger env m) + -- Makes it syntactally easier to do a bunch of these newtype Generically a = Generically {unGenerically :: a} diff --git a/src/Stackctl/CLI.hs b/src/Stackctl/CLI.hs index 612eaa8..2a0a833 100644 --- a/src/Stackctl/CLI.hs +++ b/src/Stackctl/CLI.hs @@ -7,6 +7,7 @@ module Stackctl.CLI import Stackctl.Prelude +import Blammo.Logging.LogSettings import qualified Blammo.Logging.LogSettings.Env as LoggingEnv import Control.Monad.AWS as AWS import Control.Monad.AWS.ViaReader as AWS @@ -60,7 +61,7 @@ instance HasAutoSSOOption options => HasAutoSSOOption (App options) where autoSSOOptionL = optionsL . autoSSOOptionL newtype AppT app m a = AppT - { unAppT :: ReaderT app (LoggingT (ResourceT m)) a + { unAppT :: ReaderT app (ResourceT m) a } deriving newtype ( Functor @@ -70,12 +71,13 @@ newtype AppT app m a = AppT , MonadUnliftIO , MonadResource , MonadReader app - , MonadLogger , MonadThrow , MonadCatch , MonadMask ) deriving (MonadAWS) via (ReaderAWS (AppT app m)) + deriving (MonadLogger) via (WithLogger app (ResourceT m)) + deriving (MonadLoggerIO) via (WithLogger app (ResourceT m)) runAppT :: ( MonadMask m @@ -94,36 +96,33 @@ runAppT options f = do . setLogSettingsConcurrency (Just 1) $ defaultLogSettings - logger <- - newLogger - $ adjustLogSettings - (options ^. colorOptionL) - (options ^. verboseOptionL) - envLogSettings - - app <- runResourceT $ runLoggerLoggingT logger $ do - aws <- runReaderT (handleAutoSSO options AWS.discover) logger - - App logger - <$> loadConfigOrExit - <*> pure options - <*> AWS.runEnvT fetchAwsScope aws - <*> pure aws - - let - AwsScope {..} = appAwsScope app - - context = - [ "region" .= awsRegion - , "accountId" .= awsAccountId - , "accountName" .= awsAccountName - ] - - runResourceT - $ runLoggerLoggingT app - $ flip runReaderT app - $ withThreadContext context - $ unAppT f + let logSettings = + adjustLogSettings + (options ^. colorOptionL) + (options ^. verboseOptionL) + envLogSettings + + withLogger logSettings $ \appLogger -> do + appAwsEnv <- runWithLogger appLogger $ handleAutoSSO options AWS.discover + appConfig <- runWithLogger appLogger loadConfigOrExit + appAwsScope <- AWS.runEnvT fetchAwsScope appAwsEnv + + let + AwsScope {..} = appAwsScope + + context = + [ "region" .= awsRegion + , "accountId" .= awsAccountId + , "accountName" .= awsAccountName + ] + + appOptions = options + app = App {..} + + runResourceT + $ flip runReaderT app + $ withThreadContext context + $ unAppT f adjustLogSettings :: Maybe ColorOption -> Verbosity -> LogSettings -> LogSettings diff --git a/src/Stackctl/Prelude.hs b/src/Stackctl/Prelude.hs index 614ec85..370271a 100644 --- a/src/Stackctl/Prelude.hs +++ b/src/Stackctl/Prelude.hs @@ -20,6 +20,8 @@ import RIO as X hiding ) import Blammo.Logging as X +import Blammo.Logging.Setup as X +import Blammo.Logging.ThreadContext as X import Control.Error.Util as X (hush, note) import Data.Aeson as X (ToJSON (..), object) import Data.Text as X (pack, unpack) diff --git a/src/Stackctl/StackSpecYaml.hs b/src/Stackctl/StackSpecYaml.hs index 7cb0e60..aebc651 100644 --- a/src/Stackctl/StackSpecYaml.hs +++ b/src/Stackctl/StackSpecYaml.hs @@ -107,7 +107,7 @@ instance ToJSON ParametersYaml where toJSON = object . parametersYamlPairs toEncoding = pairs . mconcat . parametersYamlPairs -parametersYamlPairs :: KeyValue kv => ParametersYaml -> [kv] +parametersYamlPairs :: KeyValue e kv => ParametersYaml -> [kv] parametersYamlPairs = map parameterYamlPair . unParametersYaml parametersYaml :: [ParameterYaml] -> ParametersYaml @@ -124,7 +124,7 @@ instance FromJSON ParameterYaml where (mkParameterYaml <$> o .: "Name" <*> o .:? "Value") <|> (mkParameterYaml <$> o .: "ParameterKey" <*> o .:? "ParameterValue") -parameterYamlPair :: KeyValue kv => ParameterYaml -> kv +parameterYamlPair :: KeyValue e kv => ParameterYaml -> kv parameterYamlPair ParameterYaml {..} = pyKey .= pyValue parameterYaml :: Parameter -> Maybe ParameterYaml @@ -235,7 +235,7 @@ instance ToJSON TagsYaml where toJSON = object . tagsYamlPairs toEncoding = pairs . mconcat . tagsYamlPairs -tagsYamlPairs :: KeyValue kv => TagsYaml -> [kv] +tagsYamlPairs :: KeyValue e kv => TagsYaml -> [kv] tagsYamlPairs = map tagYamlPair . unTagsYaml tagsYaml :: [TagYaml] -> TagsYaml @@ -251,5 +251,5 @@ instance FromJSON TagYaml where t <- newTag <$> o .: "Key" <*> o .: "Value" pure $ TagYaml t -tagYamlPair :: KeyValue kv => TagYaml -> kv +tagYamlPair :: KeyValue e kv => TagYaml -> kv tagYamlPair (TagYaml t) = Key.fromText (t ^. tag_key) .= (t ^. tag_value) diff --git a/src/Stackctl/VerboseOption.hs b/src/Stackctl/VerboseOption.hs index 9359c2c..1a6decf 100644 --- a/src/Stackctl/VerboseOption.hs +++ b/src/Stackctl/VerboseOption.hs @@ -7,6 +7,7 @@ module Stackctl.VerboseOption import Stackctl.Prelude +import Blammo.Logging.LogSettings import Blammo.Logging.LogSettings.LogLevels import Options.Applicative diff --git a/stack-lts-20.4.yaml b/stack-lts-20.4.yaml deleted file mode 100644 index 50d806a..0000000 --- a/stack-lts-20.4.yaml +++ /dev/null @@ -1,22 +0,0 @@ -resolver: lts-20.4 - -extra-deps: - - Blammo-1.1.2.3 - - cfn-flip-0.1.0.3 - - unliftio-0.2.25.0 - - - amazonka-2.0 - - amazonka-core-2.0 - - amazonka-certificatemanager-2.0 - - amazonka-cloudformation-2.0 - - amazonka-ec2-2.0 - - amazonka-ecr-2.0 - - amazonka-lambda-2.0 - - amazonka-sso-2.0 - - amazonka-sts-2.0 - - amazonka-mtl-0.1.1.0 - - - hspec-golden-0.2.1.0 - - # For amazonka-core-2.0 - - crypton-0.33 diff --git a/stack-lts-20.4.yaml.lock b/stack-lts-20.4.yaml.lock deleted file mode 100644 index 78e28b3..0000000 --- a/stack-lts-20.4.yaml.lock +++ /dev/null @@ -1,117 +0,0 @@ -# This file was autogenerated by Stack. -# You should not edit this file by hand. -# For more information, please see the documentation at: -# https://docs.haskellstack.org/en/stable/lock_files - -packages: -- completed: - hackage: Blammo-1.1.2.1@sha256:b74d553fb3557bb10381b806bd34b8bad0b800883f02dfd1cc847f58db40958c,4084 - pantry-tree: - sha256: bd28931f07beaaae8565a87d8c3b55d3e9ff5c332ae93dc32c1090a4c814e620 - size: 1567 - original: - hackage: Blammo-1.1.2.1 -- completed: - hackage: cfn-flip-0.1.0.3@sha256:8737882d818d74b29d3b1791a4df4dc89995870312374989c47c29352ea503ec,5615 - pantry-tree: - sha256: 715102dfcca7053390eda5be0504485fb93b8b84226fe373a6e62d297090d49b - size: 3139 - original: - hackage: cfn-flip-0.1.0.3 -- completed: - hackage: unliftio-0.2.25.0@sha256:d015242554890370bcbc3a575019be691d0edc279736ef97d29412fb9d0c4349,3410 - pantry-tree: - sha256: 08c62f256e740e1a78b175907c26cb06439a1b486ceb8021c5a2e4425ebb6c5b - size: 2494 - original: - hackage: unliftio-0.2.25.0 -- completed: - hackage: amazonka-2.0@sha256:3481da2fda6b210d15d41c1db7a588adf68123cfb7ea3882797a6230003259db,3505 - pantry-tree: - sha256: 01c7121bd5e4a3918a71ea6502412292c97facf20c9620f07af96e423d6437e2 - size: 1528 - original: - hackage: amazonka-2.0 -- completed: - hackage: amazonka-core-2.0@sha256:d9f0533c272ac92bd7b18699077038b6b51b3552e91b65743af4ce646286b4f8,4383 - pantry-tree: - sha256: 46e7e4de910b08ee2df98db9cda2becf388ce49510024018289a46c43e175ee0 - size: 3222 - original: - hackage: amazonka-core-2.0 -- completed: - hackage: amazonka-certificatemanager-2.0@sha256:9a203a46ec1eaae2c59aa891efa480f84411783d02ba973820d67e95cc67756c,5226 - pantry-tree: - sha256: 9ee7f26c6166f2b01f32efcf41d4a6315ff681823c698f49036f5b471ffb6e9c - size: 7191 - original: - hackage: amazonka-certificatemanager-2.0 -- completed: - hackage: amazonka-cloudformation-2.0@sha256:7a9618bf697cdaf0a51c2d7be557ad47820b926416d79f5138ff3befdbfcbafb,11870 - pantry-tree: - sha256: 177fbc16ea2fa072a7fca9f4a3b1d64f4d5e8fc7cd493e4e841f337c572745bd - size: 27257 - original: - hackage: amazonka-cloudformation-2.0 -- completed: - hackage: amazonka-ec2-2.0@sha256:9344b87d8f8328fd91023b96565e79e7676aa5e7dd40b87b3f3f3a22a9da7736,74154 - pantry-tree: - sha256: d1f2d4fce5b0664605d730d4232b25f26a5f49e3b7d07f4b282e8c36773e5ffd - size: 234434 - original: - hackage: amazonka-ec2-2.0 -- completed: - hackage: amazonka-ecr-2.0@sha256:88ec5dffb3c07f9e49eb4d9672ac62c175b6cf2c3e044ec0e4c705cd6bff3487,6925 - pantry-tree: - sha256: d0d5dc0ed4aab28f0d6183657e77985693c5ed011dd0cc40d335b6a334b1939a - size: 15627 - original: - hackage: amazonka-ecr-2.0 -- completed: - hackage: amazonka-lambda-2.0@sha256:aa74299380318b04429980eb76b7f0499a8241ff01de859042b0ff09bd7ef420,8281 - pantry-tree: - sha256: da8f346de9d1eb0fb12afa91e44f8179ac05176043919346e2e72a7880b7a9e5 - size: 21343 - original: - hackage: amazonka-lambda-2.0 -- completed: - hackage: amazonka-sso-2.0@sha256:902be13b604e4a3b51a9b8e1adc6a32f42322ae11f738a72a8c737b2d0a91a5e,2995 - pantry-tree: - sha256: f87dd959a78bf54295bd6f8c7da58f7f8f860251d5548ecb05ab758e03cba50b - size: 1817 - original: - hackage: amazonka-sso-2.0 -- completed: - hackage: amazonka-sts-2.0@sha256:5c721083e8d80883a893176de6105c27bbbd8176f467c27ac5f8d548a5e726d8,3209 - pantry-tree: - sha256: bde4691af7cac74e0a3705271b4d3ac05515863bfb6f668112e3f3950a27cb41 - size: 2880 - original: - hackage: amazonka-sts-2.0 -- completed: - hackage: amazonka-mtl-0.1.1.0@sha256:6735b3b77b38d705512480bf52e0602d35750b30b96d8a4a6dfc5025fcbe8358,6295 - pantry-tree: - sha256: e99311ec10875513e38d9402c73199bc567dddfffa6087769fe1889889627cd3 - size: 965 - original: - hackage: amazonka-mtl-0.1.1.0 -- completed: - hackage: hspec-golden-0.2.1.0@sha256:b695ae72685bbb5acd04cdd79d07c43de5ab8867e28662dd1a0002296f2a4940,2635 - pantry-tree: - sha256: d72fec5f2c0568ae958282c7a8b8f5bfba146e3e4ceee0510c0e22be5c8eb740 - size: 495 - original: - hackage: hspec-golden-0.2.1.0 -- completed: - hackage: crypton-0.33@sha256:5e92f29b9b7104d91fcdda1dec9400c9ad1f1791c231cc41ceebd783fb517dee,18202 - pantry-tree: - sha256: 38809499d7f9775ef45cd29ab5c3dc9b283a813f34c1cdc56681b24f8cf8bb4f - size: 23148 - original: - hackage: crypton-0.33 -snapshots: -- completed: - sha256: 3770dfd79f5aed67acdcc65c4e7730adddffe6dba79ea723cfb0918356fc0f94 - size: 648660 - url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/20/4.yaml - original: lts-20.4 diff --git a/stack.yaml b/stack.yaml index 71ce554..bf39091 100644 --- a/stack.yaml +++ b/stack.yaml @@ -1,6 +1,17 @@ -resolver: lts-22.6 +resolver: lts-23.7 extra-deps: - - Blammo-1.1.2.3 + - github: brendanhay/amazonka + commit: f3a7fca02fdbb832cc348e991983b1465225d50c + subdirs: + - lib/amazonka + - lib/amazonka-core + - lib/services/amazonka-cloudformation + - lib/services/amazonka-ec2 + - lib/services/amazonka-lambda + - lib/services/amazonka-sso + - lib/services/amazonka-sts + - amazonka-mtl-0.1.1.0 - cfn-flip-0.1.0.3 + - microlens-pro-0.2.0.2 diff --git a/stack.yaml.lock b/stack.yaml.lock index cb89bf7..23a363e 100644 --- a/stack.yaml.lock +++ b/stack.yaml.lock @@ -1,16 +1,100 @@ # This file was autogenerated by Stack. # You should not edit this file by hand. # For more information, please see the documentation at: -# https://docs.haskellstack.org/en/stable/lock_files +# https://docs.haskellstack.org/en/stable/topics/lock_files packages: - completed: - hackage: Blammo-1.1.2.3@sha256:33112de7280df78009ced5e815907ef62f902b8165434f538ce584df6cd9e47a,4710 + name: amazonka pantry-tree: - sha256: ec4524c3153eeb54a8554b3280e00011b21374e36df320733d0c35b8da0c9f23 - size: 1651 + sha256: cd18f37f7578d8b48e4c625df28753a644f204933a7e665541b9878876f4e05e + size: 1529 + sha256: 06f5e8430080e5a46e4489e12978725f4b01cfb896450a12a01bcde88168c7f2 + size: 34855734 + subdir: lib/amazonka + url: https://github.com/brendanhay/amazonka/archive/f3a7fca02fdbb832cc348e991983b1465225d50c.tar.gz + version: '2.0' original: - hackage: Blammo-1.1.2.3 + subdir: lib/amazonka + url: https://github.com/brendanhay/amazonka/archive/f3a7fca02fdbb832cc348e991983b1465225d50c.tar.gz +- completed: + name: amazonka-core + pantry-tree: + sha256: fbd62e7df53cf2f5b944a99d0ef024c77a10e3bde2e519fb95bcb262aed29fc4 + size: 3222 + sha256: 06f5e8430080e5a46e4489e12978725f4b01cfb896450a12a01bcde88168c7f2 + size: 34855734 + subdir: lib/amazonka-core + url: https://github.com/brendanhay/amazonka/archive/f3a7fca02fdbb832cc348e991983b1465225d50c.tar.gz + version: '2.0' + original: + subdir: lib/amazonka-core + url: https://github.com/brendanhay/amazonka/archive/f3a7fca02fdbb832cc348e991983b1465225d50c.tar.gz +- completed: + name: amazonka-cloudformation + pantry-tree: + sha256: 0cacf4a7cae64a63855bf1cce2b947084e4353f46756f36e65dd351087a7f63e + size: 27257 + sha256: 06f5e8430080e5a46e4489e12978725f4b01cfb896450a12a01bcde88168c7f2 + size: 34855734 + subdir: lib/services/amazonka-cloudformation + url: https://github.com/brendanhay/amazonka/archive/f3a7fca02fdbb832cc348e991983b1465225d50c.tar.gz + version: '2.0' + original: + subdir: lib/services/amazonka-cloudformation + url: https://github.com/brendanhay/amazonka/archive/f3a7fca02fdbb832cc348e991983b1465225d50c.tar.gz +- completed: + name: amazonka-ec2 + pantry-tree: + sha256: dc171159485af8773de82731ee1cf1df56acdf1e6c6fe76864dcf24d5d6b7e85 + size: 234434 + sha256: 06f5e8430080e5a46e4489e12978725f4b01cfb896450a12a01bcde88168c7f2 + size: 34855734 + subdir: lib/services/amazonka-ec2 + url: https://github.com/brendanhay/amazonka/archive/f3a7fca02fdbb832cc348e991983b1465225d50c.tar.gz + version: '2.0' + original: + subdir: lib/services/amazonka-ec2 + url: https://github.com/brendanhay/amazonka/archive/f3a7fca02fdbb832cc348e991983b1465225d50c.tar.gz +- completed: + name: amazonka-lambda + pantry-tree: + sha256: 249b7557046e64a2fae70acd3e7d7e20422ef7b3db49bf01d56c619e1d0a4470 + size: 21343 + sha256: 06f5e8430080e5a46e4489e12978725f4b01cfb896450a12a01bcde88168c7f2 + size: 34855734 + subdir: lib/services/amazonka-lambda + url: https://github.com/brendanhay/amazonka/archive/f3a7fca02fdbb832cc348e991983b1465225d50c.tar.gz + version: '2.0' + original: + subdir: lib/services/amazonka-lambda + url: https://github.com/brendanhay/amazonka/archive/f3a7fca02fdbb832cc348e991983b1465225d50c.tar.gz +- completed: + name: amazonka-sso + pantry-tree: + sha256: c4575f7b7cf61c3de65e43d0d77a14dfa14c47ebff5f1a3dcd2f6e1313aaaf0a + size: 1817 + sha256: 06f5e8430080e5a46e4489e12978725f4b01cfb896450a12a01bcde88168c7f2 + size: 34855734 + subdir: lib/services/amazonka-sso + url: https://github.com/brendanhay/amazonka/archive/f3a7fca02fdbb832cc348e991983b1465225d50c.tar.gz + version: '2.0' + original: + subdir: lib/services/amazonka-sso + url: https://github.com/brendanhay/amazonka/archive/f3a7fca02fdbb832cc348e991983b1465225d50c.tar.gz +- completed: + name: amazonka-sts + pantry-tree: + sha256: e0cb89013938230d257a2e546a78170dfdb6d507f37c6cb763a6cdf6290edb66 + size: 2880 + sha256: 06f5e8430080e5a46e4489e12978725f4b01cfb896450a12a01bcde88168c7f2 + size: 34855734 + subdir: lib/services/amazonka-sts + url: https://github.com/brendanhay/amazonka/archive/f3a7fca02fdbb832cc348e991983b1465225d50c.tar.gz + version: '2.0' + original: + subdir: lib/services/amazonka-sts + url: https://github.com/brendanhay/amazonka/archive/f3a7fca02fdbb832cc348e991983b1465225d50c.tar.gz - completed: hackage: amazonka-mtl-0.1.1.0@sha256:90b45a950c0e398b0e48d1447766f331c2ac3d5a72e15be2bf0be3b3c56159c3,6572 pantry-tree: @@ -25,9 +109,16 @@ packages: size: 3139 original: hackage: cfn-flip-0.1.0.3 +- completed: + hackage: microlens-pro-0.2.0.2@sha256:2fd14b7f87d6aa76700dabf65fcdda835aa329a4fdd8a44eebdf399e798af7ab,3377 + pantry-tree: + sha256: be8ac1093c45ec46d640c56c06d8826d364ad1243d601e731e37581e8579e9c3 + size: 430 + original: + hackage: microlens-pro-0.2.0.2 snapshots: - completed: - sha256: 1b4c2669e26fa828451830ed4725e4d406acc25a1fa24fcc039465dd13d7a575 - size: 714100 - url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/22/6.yaml - original: lts-22.6 + sha256: 4ef79c30b9efcf07335cb3de532983a7ac4c5a4180bc17f6212a86b09ce2ff75 + size: 680777 + url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/23/7.yaml + original: lts-23.7 diff --git a/stackctl.cabal b/stackctl.cabal index 97fbfb9..7d0a84c 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -97,7 +97,7 @@ library StandaloneDeriving TypeApplications TypeFamilies - ghc-options: -fignore-optim-changes -fwrite-ide-info -Weverything -Wno-all-missed-specialisations -Wno-missed-specialisations -Wno-missing-import-lists -Wno-missing-kind-signatures -Wno-missing-local-signatures -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module -Wno-unsafe -optP-Wno-nonportable-include-path + ghc-options: -fignore-optim-changes -fwrite-ide-info -Weverything -Wno-all-missed-specialisations -Wno-missed-specialisations -Wno-missing-import-lists -Wno-missing-kind-signatures -Wno-missing-local-signatures -Wno-missing-role-annotations -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module -Wno-unsafe -optP-Wno-nonportable-include-path build-depends: Blammo >=1.1.2.3 , Glob @@ -175,7 +175,7 @@ executable stackctl StandaloneDeriving TypeApplications TypeFamilies - ghc-options: -fignore-optim-changes -fwrite-ide-info -Weverything -Wno-all-missed-specialisations -Wno-missed-specialisations -Wno-missing-import-lists -Wno-missing-kind-signatures -Wno-missing-local-signatures -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module -Wno-unsafe -optP-Wno-nonportable-include-path -threaded -rtsopts -with-rtsopts=-N + ghc-options: -fignore-optim-changes -fwrite-ide-info -Weverything -Wno-all-missed-specialisations -Wno-missed-specialisations -Wno-missing-import-lists -Wno-missing-kind-signatures -Wno-missing-local-signatures -Wno-missing-role-annotations -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module -Wno-unsafe -optP-Wno-nonportable-include-path -threaded -rtsopts -with-rtsopts=-N build-depends: base ==4.* , stackctl @@ -229,7 +229,7 @@ test-suite spec StandaloneDeriving TypeApplications TypeFamilies - ghc-options: -fignore-optim-changes -fwrite-ide-info -Weverything -Wno-all-missed-specialisations -Wno-missed-specialisations -Wno-missing-import-lists -Wno-missing-kind-signatures -Wno-missing-local-signatures -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module -Wno-unsafe -optP-Wno-nonportable-include-path + ghc-options: -fignore-optim-changes -fwrite-ide-info -Weverything -Wno-all-missed-specialisations -Wno-missed-specialisations -Wno-missing-import-lists -Wno-missing-kind-signatures -Wno-missing-local-signatures -Wno-missing-role-annotations -Wno-missing-safe-haskell-mode -Wno-prepositive-qualified-module -Wno-unsafe -optP-Wno-nonportable-include-path build-depends: Blammo , Glob diff --git a/test/Stackctl/Test/App.hs b/test/Stackctl/Test/App.hs index 3e4f436..053d613 100644 --- a/test/Stackctl/Test/App.hs +++ b/test/Stackctl/Test/App.hs @@ -15,6 +15,7 @@ module Stackctl.Test.App import Stackctl.Prelude +import Blammo.Logging.LogSettings (defaultLogSettings) import Blammo.Logging.Logger (newTestLogger) import Control.Lens ((?~)) import Control.Monad.AWS From d4437a6694ddb9ef46a3ef9f25c1aad1f4a8d8dd Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Thu, 6 Feb 2025 15:47:00 -0500 Subject: [PATCH 153/187] fix: correct glob in finding specs The correct glob is `us-east-1/**/*.yaml`, not `us-east-1**/*.yaml`. --- src/Stackctl/AWS/Scope.hs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Stackctl/AWS/Scope.hs b/src/Stackctl/AWS/Scope.hs index 909ec8d..0b56e99 100644 --- a/src/Stackctl/AWS/Scope.hs +++ b/src/Stackctl/AWS/Scope.hs @@ -27,12 +27,14 @@ awsScopeSpecPatterns AwsScope {..} = [ compile $ "stacks" unpack (unAccountId awsAccountId) <> ".*" - unpack (fromRegion awsRegion) <> "**" + unpack (fromRegion awsRegion) + "**" "*" <.> "yaml" , compile $ "stacks" "*." <> unpack (unAccountId awsAccountId) - unpack (fromRegion awsRegion) <> "**" + unpack (fromRegion awsRegion) + "**" "*" <.> "yaml" ] From 4f21729c5c1ef448ebeefe66ab2092fede653bdd Mon Sep 17 00:00:00 2001 From: Pat Brisbin Date: Thu, 20 Feb 2025 08:00:15 -0500 Subject: [PATCH 154/187] chore(style): reformat yaml list Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- stack.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/stack.yaml b/stack.yaml index bf39091..bceca4c 100644 --- a/stack.yaml +++ b/stack.yaml @@ -4,13 +4,13 @@ extra-deps: - github: brendanhay/amazonka commit: f3a7fca02fdbb832cc348e991983b1465225d50c subdirs: - - lib/amazonka - - lib/amazonka-core - - lib/services/amazonka-cloudformation - - lib/services/amazonka-ec2 - - lib/services/amazonka-lambda - - lib/services/amazonka-sso - - lib/services/amazonka-sts + - lib/amazonka + - lib/amazonka-core + - lib/services/amazonka-cloudformation + - lib/services/amazonka-ec2 + - lib/services/amazonka-lambda + - lib/services/amazonka-sso + - lib/services/amazonka-sts - amazonka-mtl-0.1.1.0 - cfn-flip-0.1.0.3 From 1a5a43d9df17ce7042a41f26a306af2df61753f3 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Thu, 20 Feb 2025 08:42:14 -0500 Subject: [PATCH 155/187] chore(test): change order of tags yaml assertion Newer aeson seems to order differently. We don't have any real need to be robust to that, so let's just fix it to match what it is now. --- test/Stackctl/ConfigSpec.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Stackctl/ConfigSpec.hs b/test/Stackctl/ConfigSpec.hs index 3d25d07..90fd683 100644 --- a/test/Stackctl/ConfigSpec.hs +++ b/test/Stackctl/ConfigSpec.hs @@ -63,7 +63,7 @@ spec = do tags `shouldBe` toTagsYaml - [("From", "Defaults"), ("Hi", "There"), ("Keep", "Me")] + [("Hi", "There"), ("From", "Defaults"), ("Keep", "Me")] loadConfigFromLines :: MonadError ConfigError m => [ByteString] -> m Config loadConfigFromLines = loadConfigFromBytes . mconcat . map (<> "\n") From 94f3eb6a813025e98ff92cf36b2e3e903631c4cf Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Thu, 20 Feb 2025 14:56:06 -0500 Subject: [PATCH 156/187] fix(release): don't reference removed resolver This broke release of v1.7.3.1[^1]. Going back and redoing a partial release is annoying, so I'm just calling this commit a `fix` so we release v1.7.3.2. [^1]: https://github.com/freckle/stackctl/actions/runs/13443053932/job/37563302281#step:5:141 --- .github/workflows/release.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8722473..71d49fc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -77,4 +77,3 @@ jobs: FORCE_COLOR: 1 GITHUB_TOKEN: ${{ steps.token.outputs.token }} HACKAGE_KEY: ${{ secrets.HACKAGE_UPLOAD_API_KEY }} - STACK_YAML: stack-lts-20.4.yaml From 475fc44ad49a573af37fe6b9b45f7f21119f5ed3 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Mon, 24 Feb 2025 10:00:39 -0500 Subject: [PATCH 157/187] fix: use released version of semantic-release-stack-upload --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 71d49fc..885d5c3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,7 +33,7 @@ jobs: with: dry_run: true extra_plugins: | - git+https://github.com/pbrisbin/semantic-release-stack-upload.git + semantic-release-stack-upload env: FORCE_COLOR: 1 PREPARE_IN_VERIFY: 1 @@ -72,7 +72,7 @@ jobs: uses: cycjimmy/semantic-release-action@v4 with: extra_plugins: | - git+https://github.com/pbrisbin/semantic-release-stack-upload.git + semantic-release-stack-upload env: FORCE_COLOR: 1 GITHUB_TOKEN: ${{ steps.token.outputs.token }} From 294d8bbe3b154f578968359aaef317821fa353cd Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Fri, 7 Mar 2025 10:09:15 -0500 Subject: [PATCH 158/187] chore: add debug logging before AWS.discover There is a bug[^1] in the development version of Amazonka, which we have to use to get latest GHC support, that causes `newEnv` to perform a slow DNS lookup, attempting to see if it's being run on an EC2 instance. Since this happens early, before any logging, it can appear our CLIs are stuck for about 30s without any output. And bumping `LOG_LEVEL` doesn't help. Adding a message here will at least provide *some* feedback, while we seek to address this for real. [^1]: https://github.com/brendanhay/amazonka/issues/1018 --- src/Stackctl/CLI.hs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Stackctl/CLI.hs b/src/Stackctl/CLI.hs index 2a0a833..475f246 100644 --- a/src/Stackctl/CLI.hs +++ b/src/Stackctl/CLI.hs @@ -103,7 +103,9 @@ runAppT options f = do envLogSettings withLogger logSettings $ \appLogger -> do - appAwsEnv <- runWithLogger appLogger $ handleAutoSSO options AWS.discover + appAwsEnv <- runWithLogger appLogger $ handleAutoSSO options $ do + logDebug "Discovering AWS credentials" + AWS.discover appConfig <- runWithLogger appLogger loadConfigOrExit appAwsScope <- AWS.runEnvT fetchAwsScope appAwsEnv From 294140ba9a724574ebb61ccf48ba71b14b168f2d Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Mon, 10 Mar 2025 09:28:48 -0400 Subject: [PATCH 159/187] fix(deps): update amazonka This fixes the slow-down in `discover`: brendanhay/amazonka/#1029. --- stack.yaml | 2 +- stack.yaml.lock | 58 ++++++++++++++++++++++++------------------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/stack.yaml b/stack.yaml index bceca4c..7597741 100644 --- a/stack.yaml +++ b/stack.yaml @@ -2,7 +2,7 @@ resolver: lts-23.7 extra-deps: - github: brendanhay/amazonka - commit: f3a7fca02fdbb832cc348e991983b1465225d50c + commit: cf174ae30fa914439f4d1fa1c3dbd9b69b935141 # main + #1029 subdirs: - lib/amazonka - lib/amazonka-core diff --git a/stack.yaml.lock b/stack.yaml.lock index 23a363e..60debb7 100644 --- a/stack.yaml.lock +++ b/stack.yaml.lock @@ -7,94 +7,94 @@ packages: - completed: name: amazonka pantry-tree: - sha256: cd18f37f7578d8b48e4c625df28753a644f204933a7e665541b9878876f4e05e + sha256: 6a4df9d7ef86e2ecffb44ef528844a97b2339e6a6703bd304a605341c6db9842 size: 1529 - sha256: 06f5e8430080e5a46e4489e12978725f4b01cfb896450a12a01bcde88168c7f2 - size: 34855734 + sha256: bd186dab03b64bc3f4e61adafaa8b66df7c8aaff789bfe98172dedddad59e6dc + size: 34855496 subdir: lib/amazonka - url: https://github.com/brendanhay/amazonka/archive/f3a7fca02fdbb832cc348e991983b1465225d50c.tar.gz + url: https://github.com/brendanhay/amazonka/archive/cf174ae30fa914439f4d1fa1c3dbd9b69b935141.tar.gz version: '2.0' original: subdir: lib/amazonka - url: https://github.com/brendanhay/amazonka/archive/f3a7fca02fdbb832cc348e991983b1465225d50c.tar.gz + url: https://github.com/brendanhay/amazonka/archive/cf174ae30fa914439f4d1fa1c3dbd9b69b935141.tar.gz - completed: name: amazonka-core pantry-tree: sha256: fbd62e7df53cf2f5b944a99d0ef024c77a10e3bde2e519fb95bcb262aed29fc4 size: 3222 - sha256: 06f5e8430080e5a46e4489e12978725f4b01cfb896450a12a01bcde88168c7f2 - size: 34855734 + sha256: bd186dab03b64bc3f4e61adafaa8b66df7c8aaff789bfe98172dedddad59e6dc + size: 34855496 subdir: lib/amazonka-core - url: https://github.com/brendanhay/amazonka/archive/f3a7fca02fdbb832cc348e991983b1465225d50c.tar.gz + url: https://github.com/brendanhay/amazonka/archive/cf174ae30fa914439f4d1fa1c3dbd9b69b935141.tar.gz version: '2.0' original: subdir: lib/amazonka-core - url: https://github.com/brendanhay/amazonka/archive/f3a7fca02fdbb832cc348e991983b1465225d50c.tar.gz + url: https://github.com/brendanhay/amazonka/archive/cf174ae30fa914439f4d1fa1c3dbd9b69b935141.tar.gz - completed: name: amazonka-cloudformation pantry-tree: sha256: 0cacf4a7cae64a63855bf1cce2b947084e4353f46756f36e65dd351087a7f63e size: 27257 - sha256: 06f5e8430080e5a46e4489e12978725f4b01cfb896450a12a01bcde88168c7f2 - size: 34855734 + sha256: bd186dab03b64bc3f4e61adafaa8b66df7c8aaff789bfe98172dedddad59e6dc + size: 34855496 subdir: lib/services/amazonka-cloudformation - url: https://github.com/brendanhay/amazonka/archive/f3a7fca02fdbb832cc348e991983b1465225d50c.tar.gz + url: https://github.com/brendanhay/amazonka/archive/cf174ae30fa914439f4d1fa1c3dbd9b69b935141.tar.gz version: '2.0' original: subdir: lib/services/amazonka-cloudformation - url: https://github.com/brendanhay/amazonka/archive/f3a7fca02fdbb832cc348e991983b1465225d50c.tar.gz + url: https://github.com/brendanhay/amazonka/archive/cf174ae30fa914439f4d1fa1c3dbd9b69b935141.tar.gz - completed: name: amazonka-ec2 pantry-tree: sha256: dc171159485af8773de82731ee1cf1df56acdf1e6c6fe76864dcf24d5d6b7e85 size: 234434 - sha256: 06f5e8430080e5a46e4489e12978725f4b01cfb896450a12a01bcde88168c7f2 - size: 34855734 + sha256: bd186dab03b64bc3f4e61adafaa8b66df7c8aaff789bfe98172dedddad59e6dc + size: 34855496 subdir: lib/services/amazonka-ec2 - url: https://github.com/brendanhay/amazonka/archive/f3a7fca02fdbb832cc348e991983b1465225d50c.tar.gz + url: https://github.com/brendanhay/amazonka/archive/cf174ae30fa914439f4d1fa1c3dbd9b69b935141.tar.gz version: '2.0' original: subdir: lib/services/amazonka-ec2 - url: https://github.com/brendanhay/amazonka/archive/f3a7fca02fdbb832cc348e991983b1465225d50c.tar.gz + url: https://github.com/brendanhay/amazonka/archive/cf174ae30fa914439f4d1fa1c3dbd9b69b935141.tar.gz - completed: name: amazonka-lambda pantry-tree: sha256: 249b7557046e64a2fae70acd3e7d7e20422ef7b3db49bf01d56c619e1d0a4470 size: 21343 - sha256: 06f5e8430080e5a46e4489e12978725f4b01cfb896450a12a01bcde88168c7f2 - size: 34855734 + sha256: bd186dab03b64bc3f4e61adafaa8b66df7c8aaff789bfe98172dedddad59e6dc + size: 34855496 subdir: lib/services/amazonka-lambda - url: https://github.com/brendanhay/amazonka/archive/f3a7fca02fdbb832cc348e991983b1465225d50c.tar.gz + url: https://github.com/brendanhay/amazonka/archive/cf174ae30fa914439f4d1fa1c3dbd9b69b935141.tar.gz version: '2.0' original: subdir: lib/services/amazonka-lambda - url: https://github.com/brendanhay/amazonka/archive/f3a7fca02fdbb832cc348e991983b1465225d50c.tar.gz + url: https://github.com/brendanhay/amazonka/archive/cf174ae30fa914439f4d1fa1c3dbd9b69b935141.tar.gz - completed: name: amazonka-sso pantry-tree: sha256: c4575f7b7cf61c3de65e43d0d77a14dfa14c47ebff5f1a3dcd2f6e1313aaaf0a size: 1817 - sha256: 06f5e8430080e5a46e4489e12978725f4b01cfb896450a12a01bcde88168c7f2 - size: 34855734 + sha256: bd186dab03b64bc3f4e61adafaa8b66df7c8aaff789bfe98172dedddad59e6dc + size: 34855496 subdir: lib/services/amazonka-sso - url: https://github.com/brendanhay/amazonka/archive/f3a7fca02fdbb832cc348e991983b1465225d50c.tar.gz + url: https://github.com/brendanhay/amazonka/archive/cf174ae30fa914439f4d1fa1c3dbd9b69b935141.tar.gz version: '2.0' original: subdir: lib/services/amazonka-sso - url: https://github.com/brendanhay/amazonka/archive/f3a7fca02fdbb832cc348e991983b1465225d50c.tar.gz + url: https://github.com/brendanhay/amazonka/archive/cf174ae30fa914439f4d1fa1c3dbd9b69b935141.tar.gz - completed: name: amazonka-sts pantry-tree: sha256: e0cb89013938230d257a2e546a78170dfdb6d507f37c6cb763a6cdf6290edb66 size: 2880 - sha256: 06f5e8430080e5a46e4489e12978725f4b01cfb896450a12a01bcde88168c7f2 - size: 34855734 + sha256: bd186dab03b64bc3f4e61adafaa8b66df7c8aaff789bfe98172dedddad59e6dc + size: 34855496 subdir: lib/services/amazonka-sts - url: https://github.com/brendanhay/amazonka/archive/f3a7fca02fdbb832cc348e991983b1465225d50c.tar.gz + url: https://github.com/brendanhay/amazonka/archive/cf174ae30fa914439f4d1fa1c3dbd9b69b935141.tar.gz version: '2.0' original: subdir: lib/services/amazonka-sts - url: https://github.com/brendanhay/amazonka/archive/f3a7fca02fdbb832cc348e991983b1465225d50c.tar.gz + url: https://github.com/brendanhay/amazonka/archive/cf174ae30fa914439f4d1fa1c3dbd9b69b935141.tar.gz - completed: hackage: amazonka-mtl-0.1.1.0@sha256:90b45a950c0e398b0e48d1447766f331c2ac3d5a72e15be2bf0be3b3c56159c3,6572 pantry-tree: From b717e4564da8490c1f85d4b2266dc2ce94c428f2 Mon Sep 17 00:00:00 2001 From: "freckle-automation-app[bot]" <176077675+freckle-automation-app[bot]@users.noreply.github.com> Date: Mon, 17 Mar 2025 16:34:35 +0000 Subject: [PATCH 160/187] Update renovate.json --- renovate.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 renovate.json diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..bd84589 --- /dev/null +++ b/renovate.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "local>freckle/renovate-config" + ], + "minimumReleaseAge": "0 days" +} From 418cc2cd0576fee919e4a8aea9c7bc42be484cba Mon Sep 17 00:00:00 2001 From: "freckle-automation-app[bot]" <176077675+freckle-automation-app[bot]@users.noreply.github.com> Date: Mon, 31 Mar 2025 10:22:10 -0700 Subject: [PATCH 161/187] =?UTF-8?q?=F0=9F=A4=96=20Fix:=20Repository=20Comp?= =?UTF-8?q?liance=20Updates=20(#96)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Remove .github/dependabot.yml * Remove .github/workflows/mergeabot.yml --------- Co-authored-by: freckle-automation-app[bot] <176077675+freckle-automation-app[bot]@users.noreply.github.com> --- .github/dependabot.yml | 6 ------ .github/workflows/mergeabot.yml | 19 ------------------- 2 files changed, 25 deletions(-) delete mode 100644 .github/dependabot.yml delete mode 100644 .github/workflows/mergeabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 1230149..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,6 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "daily" diff --git a/.github/workflows/mergeabot.yml b/.github/workflows/mergeabot.yml deleted file mode 100644 index f1e628a..0000000 --- a/.github/workflows/mergeabot.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Mergeabot - -on: - schedule: - - cron: "0 0 * * *" - - pull_request: - -permissions: - contents: write - pull-requests: write - -jobs: - mergeabot: - runs-on: ubuntu-latest - steps: - - uses: freckle/mergeabot-action@v2 - with: - quarantine-days: 5 From b5931bf0df64e2f41cae25361b2127e0c39f9825 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 13 Apr 2025 08:15:00 +0000 Subject: [PATCH 162/187] chore(deps): update actions/create-github-app-token action to v2 (#97) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 885d5c3..73944e3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,7 +63,7 @@ jobs: - uses: actions/download-artifact@v4 - id: token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v2 with: app-id: ${{ vars.FRECKLE_AUTOMATION_APP_ID }} private-key: ${{ secrets.FRECKLE_AUTOMATION_PRIVATE_KEY }} From 90229fdeb10998f423383f372f7c78796960ba43 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Thu, 3 Jul 2025 11:18:03 -0400 Subject: [PATCH 163/187] fix(docs): add stackctl.5 to describe configuration file --- Makefile | 3 +++ man/index.txt | 2 ++ man/stackctl.1.ronn | 5 +++++ man/stackctl.5.ronn | 50 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 60 insertions(+) create mode 100644 man/stackctl.5.ronn diff --git a/Makefile b/Makefile index 5bef2ab..b1762a7 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ ARCHIVE_TARGETS := \ dist/stackctl/completion/fish \ dist/stackctl/completion/zsh \ dist/stackctl/doc/stackctl.1 \ + dist/stackctl/doc/stackctl.5 \ dist/stackctl/doc/stackctl-cat.1 \ dist/stackctl/doc/stackctl-capture.1 \ dist/stackctl/doc/stackctl-changes.1 \ @@ -56,6 +57,7 @@ install: $(INSTALL) -Dm644 completion/fish $(DESTDIR)$(PREFIX)/share/fish/vendor_completions.d/stackctl.fish $(INSTALL) -Dm644 completion/zsh $(DESTDIR)$(PREFIX)/share/zsh/site-functions/_stackctl $(INSTALL) -Dm644 doc/stackctl.1 $(DESTDIR)$(MANPREFIX)/man1/stackctl.1 + $(INSTALL) -Dm644 doc/stackctl.5 $(DESTDIR)$(MANPREFIX)/man5/stackctl.5 $(INSTALL) -Dm644 doc/stackctl-cat.1 $(DESTDIR)$(MANPREFIX)/man1/stackctl-cat.1 $(INSTALL) -Dm644 doc/stackctl-capture.1 $(DESTDIR)$(MANPREFIX)/man1/stackctl-capture.1 $(INSTALL) -Dm644 doc/stackctl-changes.1 $(DESTDIR)$(MANPREFIX)/man1/stackctl-changes.1 @@ -69,6 +71,7 @@ uninstall: $(RM) $(DESTDIR)$(PREFIX)/share/fish/vendor_completions.d/stackctl.fish $(RM) $(DESTDIR)$(PREFIX)/share/zsh/site-functions/_stackctl $(RM) $(DESTDIR)$(MANPREFIX)/man1/stackctl.1 + $(RM) $(DESTDIR)$(MANPREFIX)/man5/stackctl.5 $(RM) $(DESTDIR)$(MANPREFIX)/man1/stackctl-cat.1 $(RM) $(DESTDIR)$(MANPREFIX)/man1/stackctl-capture.1 $(RM) $(DESTDIR)$(MANPREFIX)/man1/stackctl-changes.1 diff --git a/man/index.txt b/man/index.txt index 793d09f..a697daa 100644 --- a/man/index.txt +++ b/man/index.txt @@ -1,4 +1,6 @@ # manuals included in this project: +stackctl(1) stackctl.1.ronn +stackctl(5) stackctl.5.ronn stackctl-cat(1) stackctl-cat.1.ronn stackctl-capture(1) stackctl-capture.1.ronn stackctl-changes(1) stackctl-changes.1.ronn diff --git a/man/stackctl.1.ronn b/man/stackctl.1.ronn index 3f86fa2..5f9ec2f 100644 --- a/man/stackctl.1.ronn +++ b/man/stackctl.1.ronn @@ -253,6 +253,11 @@ See stackctl-changes(1) and stackctl-deploy(1). If set, will be used as in commands that create new specifications. +## FILES + +* `.config/stackctl.yaml`: + The configuration file for Stackctl. See stackctl(5) for details. + ## AUTHOR Freckle Engineering diff --git a/man/stackctl.5.ronn b/man/stackctl.5.ronn new file mode 100644 index 0000000..dbf87dc --- /dev/null +++ b/man/stackctl.5.ronn @@ -0,0 +1,50 @@ +stackctl(5) - configuration file for Stackctl +============================================= + +## SYNOPSIS + +`.stackctl/config.yaml`
            +`.stackctl/config.yml`
            +`.stackctl.yaml`
            +`.stackctl.yml`
            + +The first path to exist will be used. + +## DESCRIPTION + +The configuration file is a YAML object with the following keys: + + * `required_version` :: : + A constraint on the version of Stackctl that must be used. The constraint + can be an exact version, or use an operator to define a minimum, maximum, or + "loose" constraint (see [EXAMPLE](#EXAMPLE)). + + * `defaults.parameters` :: >: + Parameters to use for all deploys, in the same format as the `.Parameters` + key of a stack specification (see **stackctl(1)**). + + * `defaults.tags` :: >: + Tags to use for all deploys, in the same format as the `.Tags` key of a + stack specification (see **stackctl(1)**). + +All keys are optional. + +## EXAMPLE + + + required_version: "=~ 1.7.1" # means >= 1.7.1.0 and < 1.7.2.0 + + defaults: + parameters: # list-of-object syntax + - Key: Foo + Value: Bar + - Key: Baz + Value: Bat + + tags: # object syntax (recommended) + Foo: Bar + Baz: Bat + +## SEE ALSO + +**stackctl(1)** From 3882c750d33d81eb7d6f946e51dd309ffb2174b4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 6 Aug 2025 08:39:46 +0000 Subject: [PATCH 164/187] chore(deps): update actions/download-artifact action to v5 (#99) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 73944e3..4776c5a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,7 +60,7 @@ jobs: with: persist-credentials: false - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 - id: token uses: actions/create-github-app-token@v2 From bd33989edff866bbdc2aec9ea127901f758a27d5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 11:37:17 +0000 Subject: [PATCH 165/187] chore(deps): update actions/checkout action to v5 (#100) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 6 +++--- .github/workflows/pages.yml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/restyled.yml | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c9fd844..c36d45f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: generate: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - id: generate uses: freckle/stack-action/generate-matrix@v5 outputs: @@ -25,7 +25,7 @@ jobs: fail-fast: false runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: freckle/stack-action@v5 with: stack-arguments: --stack-yaml ${{ matrix.stack-yaml }} @@ -33,7 +33,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: haskell-actions/hlint-setup@v2 - uses: haskell-actions/hlint-run@v2 with: diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index ced49d6..69638c5 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -25,7 +25,7 @@ jobs: for bin in "$HOME"/.local/share/gem/ruby/*/bin; do echo "$bin" done >>"$GITHUB_PATH" - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Generate HTML man-pages run: ronn --style toc,custom --html man/*.ronn diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4776c5a..7f300bd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,7 +26,7 @@ jobs: for bin in "$HOME"/.local/share/gem/ruby/*/bin; do echo "$bin" done >>"$GITHUB_PATH" - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - id: release uses: cycjimmy/semantic-release-action@v4 @@ -56,7 +56,7 @@ jobs: needs: build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false diff --git a/.github/workflows/restyled.yml b/.github/workflows/restyled.yml index 72720ab..fa140ac 100644 --- a/.github/workflows/restyled.yml +++ b/.github/workflows/restyled.yml @@ -15,7 +15,7 @@ jobs: restyled: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: restyled-io/actions/setup@v4 - uses: restyled-io/actions/run@v4 with: From dacbeb5d05bb99fb7cec43baf4d2aa4c020a9c73 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 23 Aug 2025 10:11:10 +0000 Subject: [PATCH 166/187] chore(deps): update actions/upload-pages-artifact action to v4 (#101) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/pages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 69638c5..eaa0a70 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -40,7 +40,7 @@ jobs: cp -v _site/stackctl.1.html _site/index.html - uses: actions/configure-pages@v5 - - uses: actions/upload-pages-artifact@v3 + - uses: actions/upload-pages-artifact@v4 with: path: _site - id: deployment From cb12d8ce40e2c36142a96a1f42cf4abee4e8b9ef Mon Sep 17 00:00:00 2001 From: Chris Martin Date: Mon, 13 Oct 2025 13:10:29 -0600 Subject: [PATCH 167/187] update nix flake (#102) --- .gitignore | 2 + flake.lock | 265 ++++++++++++++++++++++++++++++++++++----------------- flake.nix | 36 +++++--- 3 files changed, 206 insertions(+), 97 deletions(-) diff --git a/.gitignore b/.gitignore index c42eee2..20a7c51 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ man/* !man/index.txt !man/*.css !man/*.ronn +.direnv +.envrc diff --git a/flake.lock b/flake.lock index 78b3c65..2cee93a 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,21 @@ { "nodes": { + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, "flake-utils": { "inputs": { "systems": "systems" @@ -36,48 +52,128 @@ "type": "github" } }, + "flake-utils_3": { + "inputs": { + "systems": "systems_3" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, "freckle": { "inputs": { "flake-utils": "flake-utils_2", - "nixpkgs-22-11": "nixpkgs-22-11", + "haskell-openapi-code-generator": "haskell-openapi-code-generator", + "nix-github-actions": "nix-github-actions", "nixpkgs-23-05": "nixpkgs-23-05", - "nixpkgs-master-2023-05-06": "nixpkgs-master-2023-05-06", - "nixpkgs-master-2023-07-18": "nixpkgs-master-2023-07-18", - "nixpkgs-master-2023-09-15": "nixpkgs-master-2023-09-15", - "nixpkgs-master-2024-01-27": "nixpkgs-master-2024-01-27", - "nixpkgs-stable": "nixpkgs-stable", - "nixpkgs-stable-2023-07-25": "nixpkgs-stable-2023-07-25", - "nixpkgs-unstable-2023-10-21": "nixpkgs-unstable-2023-10-21", - "nixpkgs-unstable-2024-02-20": "nixpkgs-unstable-2024-02-20" + "nixpkgs-23-11": "nixpkgs-23-11", + "nixpkgs-24-05": "nixpkgs-24-05", + "nixpkgs-24-11": "nixpkgs-24-11", + "nixpkgs-25-05": "nixpkgs-25-05", + "nixpkgs-unstable": "nixpkgs-unstable" }, "locked": { "dir": "main", - "lastModified": 1708474311, - "narHash": "sha256-nO5JLvAshKODkumut9gnMrb9Uqh9PPNnWfXPM3P/kRw=", - "ref": "refs/heads/main", - "rev": "ace145f01993ddc109d86a4c47e37ffe06481df3", - "revCount": 29, - "type": "git", - "url": "ssh://git@github.com/freckle/flakes?dir=main" + "lastModified": 1760374014, + "narHash": "sha256-BoNvJ+VFtSPO8+wnyh1Qrn4XXKGxvUeb2xRNceVSFuo=", + "owner": "freckle", + "repo": "flakes", + "rev": "872341c9d85213db04b0ef7cad9e05b362af89c6", + "type": "github" }, "original": { "dir": "main", - "type": "git", - "url": "ssh://git@github.com/freckle/flakes?dir=main" + "owner": "freckle", + "repo": "flakes", + "type": "github" } }, - "nixpkgs-22-11": { + "gitignore": { + "inputs": { + "nixpkgs": [ + "freckle", + "haskell-openapi-code-generator", + "pre-commit-hooks", + "nixpkgs" + ] + }, "locked": { - "lastModified": 1688392541, - "narHash": "sha256-lHrKvEkCPTUO+7tPfjIcb7Trk6k31rz18vkyqmkeJfY=", - "owner": "nixos", + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "haskell-openapi-code-generator": { + "inputs": { + "flake-utils": "flake-utils_3", + "nixpkgs": "nixpkgs", + "pre-commit-hooks": "pre-commit-hooks" + }, + "locked": { + "lastModified": 1752914035, + "narHash": "sha256-QghINu6JPxiUyK3XSBhjgT/CvFez4hL8hGwfNvr9vPI=", + "owner": "Haskell-OpenAPI-Code-Generator", + "repo": "Haskell-OpenAPI-Client-Code-Generator", + "rev": "08fa0eb1d2baef4e3f328ae155bd0ff4ad08efcf", + "type": "github" + }, + "original": { + "owner": "Haskell-OpenAPI-Code-Generator", + "repo": "Haskell-OpenAPI-Client-Code-Generator", + "type": "github" + } + }, + "nix-github-actions": { + "inputs": { + "nixpkgs": [ + "freckle", + "nixpkgs-25-05" + ] + }, + "locked": { + "lastModified": 1737420293, + "narHash": "sha256-F1G5ifvqTpJq7fdkT34e/Jy9VCyzd5XfJ9TO8fHhJWE=", + "owner": "nix-community", + "repo": "nix-github-actions", + "rev": "f4158fa080ef4503c8f4c820967d946c2af31ec9", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nix-github-actions", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1748162331, + "narHash": "sha256-rqc2RKYTxP3tbjA+PB3VMRQNnjesrT0pEofXQTrMsS8=", + "owner": "NixOS", "repo": "nixpkgs", - "rev": "ea4c80b39be4c09702b0cb3b42eab59e2ba4f24b", + "rev": "7c43f080a7f28b2774f3b3f43234ca11661bf334", "type": "github" }, "original": { - "owner": "nixos", - "ref": "nixos-22.11", + "owner": "NixOS", + "ref": "nixos-25.05", "repo": "nixpkgs", "type": "github" } @@ -98,131 +194,135 @@ "type": "github" } }, - "nixpkgs-master-2023-05-06": { + "nixpkgs-23-11": { "locked": { - "lastModified": 1683392273, - "narHash": "sha256-pZTuxvcuDeBG+vvE1zczNyEUzlPbzXVh8Ed45Fzo+tQ=", + "lastModified": 1720535198, + "narHash": "sha256-zwVvxrdIzralnSbcpghA92tWu2DV2lwv89xZc8MTrbg=", "owner": "nixos", "repo": "nixpkgs", - "rev": "16b3b0c53b1ee8936739f8c588544e7fcec3fc60", + "rev": "205fd4226592cc83fd4c0885a3e4c9c400efabb5", "type": "github" }, "original": { "owner": "nixos", + "ref": "nixos-23.11", "repo": "nixpkgs", - "rev": "16b3b0c53b1ee8936739f8c588544e7fcec3fc60", "type": "github" } }, - "nixpkgs-master-2023-07-18": { + "nixpkgs-24-05": { "locked": { - "lastModified": 1689680872, - "narHash": "sha256-brNix2+ihJSzCiKwLafbyejrHJZUP0Fy6z5+xMOC27M=", + "lastModified": 1735563628, + "narHash": "sha256-OnSAY7XDSx7CtDoqNh8jwVwh4xNL/2HaJxGjryLWzX8=", "owner": "nixos", "repo": "nixpkgs", - "rev": "08700de174bc6235043cb4263b643b721d936bdb", + "rev": "b134951a4c9f3c995fd7be05f3243f8ecd65d798", "type": "github" }, "original": { "owner": "nixos", + "ref": "nixos-24.05", "repo": "nixpkgs", - "rev": "08700de174bc6235043cb4263b643b721d936bdb", "type": "github" } }, - "nixpkgs-master-2023-09-15": { + "nixpkgs-24-11": { "locked": { - "lastModified": 1694760568, - "narHash": "sha256-3G07BiXrp2YQKxdcdms22MUx6spc6A++MSePtatCYuI=", + "lastModified": 1751274312, + "narHash": "sha256-/bVBlRpECLVzjV19t5KMdMFWSwKLtb5RyXdjz3LJT+g=", "owner": "nixos", "repo": "nixpkgs", - "rev": "46688f8eb5cd6f1298d873d4d2b9cf245e09e88e", + "rev": "50ab793786d9de88ee30ec4e4c24fb4236fc2674", "type": "github" }, "original": { "owner": "nixos", + "ref": "nixos-24.11", "repo": "nixpkgs", - "rev": "46688f8eb5cd6f1298d873d4d2b9cf245e09e88e", "type": "github" } }, - "nixpkgs-master-2024-01-27": { + "nixpkgs-25-05": { "locked": { - "lastModified": 1706367331, - "narHash": "sha256-AqgkGHRrI6h/8FWuVbnkfFmXr4Bqsr4fV23aISqj/xg=", + "lastModified": 1760139962, + "narHash": "sha256-4xggC56Rub3WInz5eD7EZWXuLXpNvJiUPahGtMkwtuc=", "owner": "nixos", "repo": "nixpkgs", - "rev": "160b762eda6d139ac10ae081f8f78d640dd523eb", + "rev": "7e297ddff44a3cc93673bb38d0374df8d0ad73e4", "type": "github" }, "original": { "owner": "nixos", + "ref": "nixos-25.05", "repo": "nixpkgs", - "rev": "160b762eda6d139ac10ae081f8f78d640dd523eb", "type": "github" } }, - "nixpkgs-stable": { + "nixpkgs-unstable": { "locked": { - "lastModified": 1708294118, - "narHash": "sha256-evZzmLW7qoHXf76VCepvun1esZDxHfVRFUJtumD7L2M=", + "lastModified": 1760284886, + "narHash": "sha256-TK9Kr0BYBQ/1P5kAsnNQhmWWKgmZXwUQr4ZMjCzWf2c=", "owner": "nixos", "repo": "nixpkgs", - "rev": "e0da498ad77ac8909a980f07eff060862417ccf7", + "rev": "cf3f5c4def3c7b5f1fc012b3d839575dbe552d43", "type": "github" }, "original": { "owner": "nixos", - "ref": "nixos-23.11", + "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" } }, - "nixpkgs-stable-2023-07-25": { + "nixpkgs_2": { "locked": { - "lastModified": 1690271650, - "narHash": "sha256-qwdsW8DBY1qH+9luliIH7VzgwvL+ZGI3LZWC0LTiDMI=", - "owner": "nixos", + "lastModified": 1730768919, + "narHash": "sha256-8AKquNnnSaJRXZxc5YmF/WfmxiHX6MMZZasRP6RRQkE=", + "owner": "NixOS", "repo": "nixpkgs", - "rev": "6dc93f0daec55ee2f441da385aaf143863e3d671", + "rev": "a04d33c0c3f1a59a2c1cb0c6e34cd24500e5a1dc", "type": "github" }, "original": { - "owner": "nixos", + "owner": "NixOS", + "ref": "nixpkgs-unstable", "repo": "nixpkgs", - "rev": "6dc93f0daec55ee2f441da385aaf143863e3d671", "type": "github" } }, - "nixpkgs-unstable-2023-10-21": { + "nixpkgs_3": { "locked": { - "lastModified": 1697793076, - "narHash": "sha256-02e7sCuqLtkyRgrZmdOyvAcQTQdcXj+vpyp9bca6cY4=", + "lastModified": 1760139962, + "narHash": "sha256-4xggC56Rub3WInz5eD7EZWXuLXpNvJiUPahGtMkwtuc=", "owner": "nixos", "repo": "nixpkgs", - "rev": "038b2922be3fc096e1d456f93f7d0f4090628729", + "rev": "7e297ddff44a3cc93673bb38d0374df8d0ad73e4", "type": "github" }, "original": { "owner": "nixos", + "ref": "nixos-25.05", "repo": "nixpkgs", - "rev": "038b2922be3fc096e1d456f93f7d0f4090628729", "type": "github" } }, - "nixpkgs-unstable-2024-02-20": { + "pre-commit-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": "nixpkgs_2" + }, "locked": { - "lastModified": 1708296515, - "narHash": "sha256-FyF489fYNAUy7b6dkYV6rGPyzp+4tThhr80KNAaF/yY=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "b98a4e1746acceb92c509bc496ef3d0e5ad8d4aa", + "lastModified": 1742649964, + "narHash": "sha256-DwOTp7nvfi8mRfuL1escHDXabVXFGT1VlPD1JHrtrco=", + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "rev": "dcf5072734cb576d2b0c59b2ac44f5050b5eac82", "type": "github" }, "original": { - "owner": "nixos", - "repo": "nixpkgs", - "rev": "b98a4e1746acceb92c509bc496ef3d0e5ad8d4aa", + "owner": "cachix", + "repo": "pre-commit-hooks.nix", "type": "github" } }, @@ -230,26 +330,25 @@ "inputs": { "flake-utils": "flake-utils", "freckle": "freckle", - "stable": "stable" + "nixpkgs": "nixpkgs_3" } }, - "stable": { + "systems": { "locked": { - "lastModified": 1712168706, - "narHash": "sha256-XP24tOobf6GGElMd0ux90FEBalUtw6NkBSVh/RlA6ik=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "1487bdea619e4a7a53a4590c475deabb5a9d1bfb", + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", "type": "github" }, "original": { - "owner": "nixos", - "ref": "nixos-23.11", - "repo": "nixpkgs", + "owner": "nix-systems", + "repo": "default", "type": "github" } }, - "systems": { + "systems_2": { "locked": { "lastModified": 1681028828, "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", @@ -264,7 +363,7 @@ "type": "github" } }, - "systems_2": { + "systems_3": { "locked": { "lastModified": 1681028828, "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", diff --git a/flake.nix b/flake.nix index 74312be..7ff838f 100644 --- a/flake.nix +++ b/flake.nix @@ -1,15 +1,12 @@ { inputs = { - stable.url = "github:nixos/nixpkgs/nixos-23.11"; - freckle.url = "git+ssh://git@github.com/freckle/flakes?dir=main"; + nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05"; + freckle.url = "github:freckle/flakes?dir=main"; flake-utils.url = "github:numtide/flake-utils"; }; outputs = inputs: inputs.flake-utils.lib.eachDefaultSystem (system: let - nixpkgsArgs = { inherit system; config = { }; }; - nixpkgs = { - stable = import inputs.stable nixpkgsArgs; - }; + nixpkgs = inputs.nixpkgs.legacyPackages.${system}; freckle = inputs.freckle.packages.${system}; freckleLib = inputs.freckle.lib.${system}; in @@ -17,31 +14,31 @@ packages = { awscli = freckle.aws-cli-2-11-x; - cabal = nixpkgs.stable.cabal-install; + cabal = nixpkgs.cabal-install; fourmolu = freckle.fourmolu-0-13-x; ghc = freckleLib.haskellBundle { - ghcVersion = "ghc-9-6-3"; + ghcVersion = "ghc-9-8-4"; packageSelection = p: [ ]; enableHLS = true; }; hlint = - nixpkgs.stable.haskell.lib.justStaticExecutables - nixpkgs.stable.hlint; + nixpkgs.haskell.lib.justStaticExecutables + nixpkgs.hlint; - stack = nixpkgs.stable.writeShellApplication { + stack = nixpkgs.writeShellApplication { name = "stack"; text = '' - ${nixpkgs.stable.stack}/bin/stack --system-ghc --no-nix "$@" + ${nixpkgs.stack}/bin/stack --system-ghc --no-nix "$@" ''; } ; }; - devShells.default = nixpkgs.stable.mkShell { - buildInputs = with (nixpkgs.stable); [ + devShells.default = nixpkgs.mkShell { + buildInputs = with (nixpkgs); [ pcre pcre.dev zlib @@ -62,4 +59,15 @@ ''; }; }); + + nixConfig = { + extra-substituters = [ + "https://freckle.cachix.org" + "https://freckle-private.cachix.org" + ]; + extra-trusted-public-keys = [ + "freckle.cachix.org-1:WnI1pZdwLf2vnP9Fx7OGbVSREqqi4HM2OhNjYmZ7odo=" + "freckle-private.cachix.org-1:zbTfpeeq5YBCPOjheu0gLyVPVeM6K2dc1e8ei8fE0AI=" + ]; + }; } From 90d30025304002c924d69583402a8831fa141069 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Fri, 17 Oct 2025 11:33:41 -0400 Subject: [PATCH 168/187] chore(docs): update --directory help text --- man/stackctl.1.ronn | 3 ++- src/Stackctl/DirectoryOption.hs | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/man/stackctl.1.ronn b/man/stackctl.1.ronn index 5f9ec2f..8863fb2 100644 --- a/man/stackctl.1.ronn +++ b/man/stackctl.1.ronn @@ -8,7 +8,8 @@ stackctl(1) - manage CloudFormation Stacks through specifications ## OPTIONS * `-d`, `--directory`=: - Where to find specifications. Default is `.`. + Use the stack collection located at (default: current working + directory). * `--filter`=: Restrict specifications to those whose paths match any given . diff --git a/src/Stackctl/DirectoryOption.hs b/src/Stackctl/DirectoryOption.hs index a156a84..9acb7d9 100644 --- a/src/Stackctl/DirectoryOption.hs +++ b/src/Stackctl/DirectoryOption.hs @@ -30,7 +30,7 @@ instance HasDirectoryOption DirectoryOption where envDirectoryOption :: Env.Parser Env.Error DirectoryOption envDirectoryOption = Env.var (Env.str <=< Env.nonempty) "DIRECTORY" - $ Env.help "Operate on specifications in this directory" + $ Env.help directoryHelp directoryOption :: Parser DirectoryOption directoryOption = @@ -39,6 +39,10 @@ directoryOption = [ short 'd' , long "directory" , metavar "PATH" - , help "Operate on specifications in PATH" + , help directoryHelp , action "directory" ] + +directoryHelp :: String +directoryHelp = + "Use the stack collection located at PATH (default: current working directory)" From 8537f464e581f878b99f05c64c3827471a8f3ead Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Fri, 17 Oct 2025 11:35:27 -0400 Subject: [PATCH 169/187] fix(docs): mention --filter in all subcommand help optparse-applicative offers a `header` or a `footer` info modifier, but they both put the text in the wrong place: either above or below everything. To get the text to appear after the one-line description, but before the options, we need to manually append it. Here is the updated help, ```console % stackctl deploy --help Usage: stackctl deploy [-p|--parameter KEY=[VALUE]] [-t|--tag KEY=[VALUE]] [--save-change-sets DIRECTORY] [--no-confirm] [--no-remove] [--clean] Deploy specifications By default, this will operate on the entire stack collection. To operate on a specific stack or set of stacks, use the --filter argument to filter the collection by file path. Available options: -p,--parameter KEY=[VALUE] Override the given Parameter for this operation -t,--tag KEY=[VALUE] Override the given Tag for this operation --save-change-sets DIRECTORY Save executed changesets to DIRECTORY --no-confirm Don't confirm changes before executing --no-remove Don't delete removed Stacks --clean Remove all changesets from Stack after deploy -h,--help Show this help text Global options: -d,--directory PATH Use the stack collection located at PATH (default: current working directory) --filter PATTERN[,PATTERN] Filter specifications to match PATTERN(s) --color auto|always|never When to colorize output -v,--verbose Increase verbosity (can be passed multiple times) --auto-sso WHEN Automatically run aws-sso-login if necessary? ``` NOTE: `fullDescr` was removed because it is the default. `helpShowGlobals` was added because it was an oversight to not have it before. --- package.yaml | 1 + src/Stackctl/Subcommand.hs | 19 +++++++++++++++++-- stackctl.cabal | 3 ++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/package.yaml b/package.yaml index 7cd0871..865ec01 100644 --- a/package.yaml +++ b/package.yaml @@ -88,6 +88,7 @@ library: - monad-logger - mtl - optparse-applicative + - prettyprinter - resourcet - rio - semigroups diff --git a/src/Stackctl/Subcommand.hs b/src/Stackctl/Subcommand.hs index eb84864..ec7d33c 100644 --- a/src/Stackctl/Subcommand.hs +++ b/src/Stackctl/Subcommand.hs @@ -10,6 +10,8 @@ import Stackctl.Prelude import qualified Env import Options.Applicative +import Prettyprinter (pretty, vsep) +import Prettyprinter.Util (reflow) import Stackctl.AWS (handlingServiceError) import Stackctl.AutoSSO import Stackctl.CLI @@ -46,7 +48,9 @@ runSubcommand' title parseEnv parseCLI sp = do (options, act) <- applyEnv <$> Env.parse (Env.header $ unpack title) parseEnv - <*> execParser (withInfo title $ (,) <$> parseCLI <*> subparser sp) + <*> customExecParser + (prefs helpShowGlobals) + (withInfo title $ (,) <$> parseCLI <*> subparser sp) act options where @@ -78,4 +82,15 @@ runAppSubcommand f subOptions options = $ f subOptions withInfo :: Text -> Parser a -> ParserInfo a -withInfo d p = info (p <**> helper) $ progDesc (unpack d) <> fullDesc +withInfo d p = + info (p <**> helper) + $ progDescDoc + $ Just + $ vsep + [ pretty d + , "" + , reflow + $ "By default, this will operate on the entire stack collection. To" + <> " operate on a specific stack or set of stacks, use the --filter" + <> " argument to filter the collection by file path." + ] diff --git a/stackctl.cabal b/stackctl.cabal index 7d0a84c..c920407 100644 --- a/stackctl.cabal +++ b/stackctl.cabal @@ -1,6 +1,6 @@ cabal-version: 1.18 --- This file has been generated from package.yaml by hpack version 0.37.0. +-- This file has been generated from package.yaml by hpack version 0.38.1. -- -- see: https://github.com/sol/hpack @@ -128,6 +128,7 @@ library , monad-logger , mtl , optparse-applicative + , prettyprinter , resourcet , rio , semigroups From 32b1386ade0b48ffde8028bafa0ef086de74d026 Mon Sep 17 00:00:00 2001 From: patrick brisbin Date: Fri, 17 Oct 2025 14:31:36 -0400 Subject: [PATCH 170/187] fix: catch AuthError to an informative message Before, ```console % AWS_PROFILE=x stack exec -- stackctl cat CredentialChainExhausted ``` Now, ```console % AWS_PROFILE=x stack exec -- stackctl cat 2025-10-17 18:31:44 [error ] No AWS credentials were found in your environment. For details of where stackctl looks for credentials, see: https://hackage.haskell.org/package/amazonka-2.0/docs/Amazonka-Auth.html#v:discover exception=CredentialChainExhausted ``` --- src/Stackctl/AWS/Core.hs | 19 +++++++++++++++++++ src/Stackctl/CLI.hs | 10 +++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/Stackctl/AWS/Core.hs b/src/Stackctl/AWS/Core.hs index eff7c88..1cd8657 100644 --- a/src/Stackctl/AWS/Core.hs +++ b/src/Stackctl/AWS/Core.hs @@ -12,6 +12,7 @@ module Stackctl.AWS.Core , withAssumedRole -- * Error-handling + , handlingAuthError , handlingServiceError , formatServiceError @@ -38,6 +39,7 @@ import Amazonka , serviceError_code , serviceError_message , serviceError_requestId + , _AuthError , _Sensitive , _ServiceError ) @@ -49,6 +51,7 @@ import qualified Amazonka.Env as Amazonka import Amazonka.STS.AssumeRole import Control.Monad.AWS import Control.Monad.Logger (defaultLoc, toLogStr) +import qualified Data.Text as T import Data.Typeable (typeRep) import Stackctl.AWS.Orphans () import UnliftIO.Exception.Lens (handling) @@ -149,6 +152,22 @@ newtype AccountId = AccountId } deriving newtype (Eq, Ord, Show, ToJSON) +-- | Handle 'AuthError', log it and 'exitFailure' +handlingAuthError :: (MonadUnliftIO m, MonadLogger m) => m a -> m a +handlingAuthError = + handling _AuthError $ \e -> do + logError $ msg :# ["exception" .= displayException e] + exitFailure + where + msg = + "No AWS credentials were found in your environment." + <> continuation "For details of where stackctl looks for credentials, see:" + <> continuation + "https://hackage.haskell.org/package/amazonka-2.0/docs/Amazonka-Auth.html#v:discover" + + -- Shift the continuation lines to line up with the first message line + continuation x = "\n" <> T.replicate 32 " " <> x + -- | Handle 'ServiceError', log it and 'exitFailure' -- -- This is useful at the top-level of the app, where we'd be crashing anyway. It diff --git a/src/Stackctl/CLI.hs b/src/Stackctl/CLI.hs index 475f246..e12671e 100644 --- a/src/Stackctl/CLI.hs +++ b/src/Stackctl/CLI.hs @@ -13,6 +13,7 @@ import Control.Monad.AWS as AWS import Control.Monad.AWS.ViaReader as AWS import Control.Monad.Catch (MonadCatch) import Control.Monad.Trans.Resource (MonadResource, ResourceT, runResourceT) +import Stackctl.AWS.Core (handlingAuthError) import qualified Stackctl.AWS.Core as AWS import Stackctl.AWS.Scope import Stackctl.AutoSSO @@ -103,9 +104,12 @@ runAppT options f = do envLogSettings withLogger logSettings $ \appLogger -> do - appAwsEnv <- runWithLogger appLogger $ handleAutoSSO options $ do - logDebug "Discovering AWS credentials" - AWS.discover + appAwsEnv <- runWithLogger appLogger + $ handleAutoSSO options + $ handlingAuthError + $ do + logDebug "Discovering AWS credentials" + AWS.discover appConfig <- runWithLogger appLogger loadConfigOrExit appAwsScope <- AWS.runEnvT fetchAwsScope appAwsEnv From aae10aac7dd7a1e65f32a2c412b00098be2a0c65 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 25 Oct 2025 09:04:38 +0000 Subject: [PATCH 171/187] chore(deps): update github artifact actions (#109) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7f300bd..92c35a3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,7 +46,7 @@ jobs: - run: | make install.check # creates dist/stackctl.tar.gz cp -v dist/stackctl.tar.gz stackctl-${{ matrix.suffix }}.tar.gz - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v5 with: name: ${{ matrix.os }}-binaries path: "stackctl-*.tar.gz" @@ -60,7 +60,7 @@ jobs: with: persist-credentials: false - - uses: actions/download-artifact@v5 + - uses: actions/download-artifact@v6 - id: token uses: actions/create-github-app-token@v2 From 3b5d4fc6382740bbb63f33d89a2a3f0405e4bd76 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 21 Nov 2025 10:54:44 +0000 Subject: [PATCH 172/187] chore(deps): update actions/checkout action to v6 (#110) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 6 +++--- .github/workflows/pages.yml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/restyled.yml | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c36d45f..872fb2e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: generate: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - id: generate uses: freckle/stack-action/generate-matrix@v5 outputs: @@ -25,7 +25,7 @@ jobs: fail-fast: false runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: freckle/stack-action@v5 with: stack-arguments: --stack-yaml ${{ matrix.stack-yaml }} @@ -33,7 +33,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: haskell-actions/hlint-setup@v2 - uses: haskell-actions/hlint-run@v2 with: diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index eaa0a70..43c95a7 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -25,7 +25,7 @@ jobs: for bin in "$HOME"/.local/share/gem/ruby/*/bin; do echo "$bin" done >>"$GITHUB_PATH" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Generate HTML man-pages run: ronn --style toc,custom --html man/*.ronn diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 92c35a3..a775104 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,7 +26,7 @@ jobs: for bin in "$HOME"/.local/share/gem/ruby/*/bin; do echo "$bin" done >>"$GITHUB_PATH" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - id: release uses: cycjimmy/semantic-release-action@v4 @@ -56,7 +56,7 @@ jobs: needs: build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false diff --git a/.github/workflows/restyled.yml b/.github/workflows/restyled.yml index fa140ac..b09c400 100644 --- a/.github/workflows/restyled.yml +++ b/.github/workflows/restyled.yml @@ -15,7 +15,7 @@ jobs: restyled: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: restyled-io/actions/setup@v4 - uses: restyled-io/actions/run@v4 with: From a6763d4c6d9a2180b6bfc4ae9c977eb9edbd7683 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 14 Dec 2025 08:33:47 +0000 Subject: [PATCH 173/187] chore(deps): update github artifact actions (#111) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a775104..3ba06a7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,7 +46,7 @@ jobs: - run: | make install.check # creates dist/stackctl.tar.gz cp -v dist/stackctl.tar.gz stackctl-${{ matrix.suffix }}.tar.gz - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@v6 with: name: ${{ matrix.os }}-binaries path: "stackctl-*.tar.gz" @@ -60,7 +60,7 @@ jobs: with: persist-credentials: false - - uses: actions/download-artifact@v6 + - uses: actions/download-artifact@v7 - id: token uses: actions/create-github-app-token@v2 From 71308fb1e99949d382d1bdfb71acca7bc311731f Mon Sep 17 00:00:00 2001 From: "freckle-automation-app[bot]" <176077675+freckle-automation-app[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 09:06:15 -0800 Subject: [PATCH 174/187] Remove .github/workflows/add-asana-comment.yml (#112) Co-authored-by: freckle-automation-app[bot] <176077675+freckle-automation-app[bot]@users.noreply.github.com> --- .github/workflows/add-asana-comment.yml | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 .github/workflows/add-asana-comment.yml diff --git a/.github/workflows/add-asana-comment.yml b/.github/workflows/add-asana-comment.yml deleted file mode 100644 index aaa3f6d..0000000 --- a/.github/workflows/add-asana-comment.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Asana - -on: - pull_request: - types: [opened] - -jobs: - link-asana-task: - if: ${{ github.actor != 'dependabot[bot]' }} - runs-on: ubuntu-latest - steps: - - uses: Asana/create-app-attachment-github-action@v1.3 - id: postAttachment - with: - asana-secret: ${{ secrets.ASANA_API_ACCESS_KEY }} - - run: echo "Status is ${{ steps.postAttachment.outputs.status }}" From 5ba7acf53b09779857aba85344a304dc7c9a358a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 09:02:16 +0000 Subject: [PATCH 175/187] chore(deps): update github artifact actions (#113) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3ba06a7..ecfe81d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,7 +46,7 @@ jobs: - run: | make install.check # creates dist/stackctl.tar.gz cp -v dist/stackctl.tar.gz stackctl-${{ matrix.suffix }}.tar.gz - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v7 with: name: ${{ matrix.os }}-binaries path: "stackctl-*.tar.gz" @@ -60,7 +60,7 @@ jobs: with: persist-credentials: false - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 - id: token uses: actions/create-github-app-token@v2 From c3b56f4411674f51cf4c5a1c2a6217d1173df836 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 15 Mar 2026 09:36:57 +0000 Subject: [PATCH 176/187] chore(deps): update actions/create-github-app-token action to v3 (#114) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ecfe81d..8c273c8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,7 +63,7 @@ jobs: - uses: actions/download-artifact@v8 - id: token - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 with: app-id: ${{ vars.FRECKLE_AUTOMATION_APP_ID }} private-key: ${{ secrets.FRECKLE_AUTOMATION_PRIVATE_KEY }} From 70e1325093dde068702a032716cd49c897abab1c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 09:01:48 +0000 Subject: [PATCH 177/187] chore(deps): update actions/deploy-pages action to v5 (#115) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/pages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 43c95a7..4364449 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -44,4 +44,4 @@ jobs: with: path: _site - id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5 From 91d6707c5e0021c294f34b6c15056a425fdc4469 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 09:35:00 +0000 Subject: [PATCH 178/187] chore(deps): update actions/configure-pages action to v6 (#116) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/pages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 4364449..2486c43 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -39,7 +39,7 @@ jobs: cp -v man/*.html _site/ cp -v _site/stackctl.1.html _site/index.html - - uses: actions/configure-pages@v5 + - uses: actions/configure-pages@v6 - uses: actions/upload-pages-artifact@v4 with: path: _site From ebc6d1f00841229ac6a87ceae8d282470da59da8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 08:56:40 +0000 Subject: [PATCH 179/187] chore(deps): update actions/upload-pages-artifact action to v5 (#117) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/pages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 2486c43..3e4a3bf 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -40,7 +40,7 @@ jobs: cp -v _site/stackctl.1.html _site/index.html - uses: actions/configure-pages@v6 - - uses: actions/upload-pages-artifact@v4 + - uses: actions/upload-pages-artifact@v5 with: path: _site - id: deployment From c1ea8522cab5f0c7423461fea473509a7dfa0e7f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 12:00:42 +0000 Subject: [PATCH 180/187] chore(deps): update cycjimmy/semantic-release-action action to v6 (#118) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8c273c8..9ac9bb0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,7 +29,7 @@ jobs: - uses: actions/checkout@v6 - id: release - uses: cycjimmy/semantic-release-action@v4 + uses: cycjimmy/semantic-release-action@v6.0.0 with: dry_run: true extra_plugins: | @@ -69,7 +69,7 @@ jobs: private-key: ${{ secrets.FRECKLE_AUTOMATION_PRIVATE_KEY }} - id: release - uses: cycjimmy/semantic-release-action@v4 + uses: cycjimmy/semantic-release-action@v6.0.0 with: extra_plugins: | semantic-release-stack-upload From 25e3e9aa6caa77b1952707cc8d6d28bc8d92380f Mon Sep 17 00:00:00 2001 From: "freckle-automation-app[bot]" <176077675+freckle-automation-app[bot]@users.noreply.github.com> Date: Tue, 26 May 2026 09:39:37 -0400 Subject: [PATCH 181/187] Fix: Repository Compliance Updates This PR adds the following compliance configurations: * mergeabot --- .github/workflows/mergeabot.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/workflows/mergeabot.yml diff --git a/.github/workflows/mergeabot.yml b/.github/workflows/mergeabot.yml new file mode 100644 index 0000000..90e154b --- /dev/null +++ b/.github/workflows/mergeabot.yml @@ -0,0 +1,19 @@ +name: Mergeabot + +on: + schedule: + - cron: "0 0 * * *" + + pull_request: + +permissions: + contents: write + pull-requests: write + +jobs: + mergeabot: + runs-on: ubuntu-latest + steps: + - uses: freckle/mergeabot-action@v2 + with: + quarantine-days: -1 From f13f53ed25f93472ea7b97afbad733ea9fb84038 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 20 Jun 2026 09:06:54 +0000 Subject: [PATCH 182/187] chore(deps): update actions/checkout action to v7 (#122) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 6 +++--- .github/workflows/pages.yml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/restyled.yml | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 872fb2e..0742ac4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: generate: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - id: generate uses: freckle/stack-action/generate-matrix@v5 outputs: @@ -25,7 +25,7 @@ jobs: fail-fast: false runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: freckle/stack-action@v5 with: stack-arguments: --stack-yaml ${{ matrix.stack-yaml }} @@ -33,7 +33,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: haskell-actions/hlint-setup@v2 - uses: haskell-actions/hlint-run@v2 with: diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 3e4a3bf..b013786 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -25,7 +25,7 @@ jobs: for bin in "$HOME"/.local/share/gem/ruby/*/bin; do echo "$bin" done >>"$GITHUB_PATH" - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Generate HTML man-pages run: ronn --style toc,custom --html man/*.ronn diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9ac9bb0..5632f06 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,7 +26,7 @@ jobs: for bin in "$HOME"/.local/share/gem/ruby/*/bin; do echo "$bin" done >>"$GITHUB_PATH" - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - id: release uses: cycjimmy/semantic-release-action@v6.0.0 @@ -56,7 +56,7 @@ jobs: needs: build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false diff --git a/.github/workflows/restyled.yml b/.github/workflows/restyled.yml index b09c400..c9a42d0 100644 --- a/.github/workflows/restyled.yml +++ b/.github/workflows/restyled.yml @@ -15,7 +15,7 @@ jobs: restyled: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: restyled-io/actions/setup@v4 - uses: restyled-io/actions/run@v4 with: From db456b327b10d332a2c8077244e51bd8ce2d2e5d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 30 Jul 2026 08:44:41 +0000 Subject: [PATCH 183/187] chore(deps): update freckle/mergeabot-action action to v3 (#124) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/mergeabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mergeabot.yml b/.github/workflows/mergeabot.yml index 90e154b..6cef288 100644 --- a/.github/workflows/mergeabot.yml +++ b/.github/workflows/mergeabot.yml @@ -14,6 +14,6 @@ jobs: mergeabot: runs-on: ubuntu-latest steps: - - uses: freckle/mergeabot-action@v2 + - uses: freckle/mergeabot-action@v3.0.0 with: quarantine-days: -1 From 12c070b3759f188a6abaf509d91b13a4300fdb37 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 08:35:51 +0000 Subject: [PATCH 184/187] chore(deps): update freckle/mergeabot-action action to v3.1.0 (#125) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/mergeabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mergeabot.yml b/.github/workflows/mergeabot.yml index 6cef288..1fca477 100644 --- a/.github/workflows/mergeabot.yml +++ b/.github/workflows/mergeabot.yml @@ -14,6 +14,6 @@ jobs: mergeabot: runs-on: ubuntu-latest steps: - - uses: freckle/mergeabot-action@v3.0.0 + - uses: freckle/mergeabot-action@v3.1.0 with: quarantine-days: -1 From 4d4b24ba53ca0c37b434c06d728087fbf70485fe Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:40:06 +0000 Subject: [PATCH 185/187] chore(deps): update freckle/mergeabot-action action to v3.1.1 (#126) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/mergeabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mergeabot.yml b/.github/workflows/mergeabot.yml index 1fca477..5eee92f 100644 --- a/.github/workflows/mergeabot.yml +++ b/.github/workflows/mergeabot.yml @@ -14,6 +14,6 @@ jobs: mergeabot: runs-on: ubuntu-latest steps: - - uses: freckle/mergeabot-action@v3.1.0 + - uses: freckle/mergeabot-action@v3.1.1 with: quarantine-days: -1 From 47ef16a0d06ca59ffeaf83e8c7855bf157360249 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 15 Aug 2026 08:29:22 +0000 Subject: [PATCH 186/187] chore(deps): update freckle/mergeabot-action action to v3.1.2 (#127) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/mergeabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mergeabot.yml b/.github/workflows/mergeabot.yml index 5eee92f..434cb69 100644 --- a/.github/workflows/mergeabot.yml +++ b/.github/workflows/mergeabot.yml @@ -14,6 +14,6 @@ jobs: mergeabot: runs-on: ubuntu-latest steps: - - uses: freckle/mergeabot-action@v3.1.1 + - uses: freckle/mergeabot-action@v3.1.2 with: quarantine-days: -1 From 19bee5da501894d2e6ec825769b5153f1d89d567 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 08:53:58 +0000 Subject: [PATCH 187/187] chore(deps): update freckle/mergeabot-action action to v3.2.0 (#128) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/mergeabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mergeabot.yml b/.github/workflows/mergeabot.yml index 434cb69..d29d504 100644 --- a/.github/workflows/mergeabot.yml +++ b/.github/workflows/mergeabot.yml @@ -14,6 +14,6 @@ jobs: mergeabot: runs-on: ubuntu-latest steps: - - uses: freckle/mergeabot-action@v3.1.2 + - uses: freckle/mergeabot-action@v3.2.0 with: quarantine-days: -1