-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathStackSpec.hs
More file actions
204 lines (174 loc) · 5.95 KB
/
Copy pathStackSpec.hs
File metadata and controls
204 lines (174 loc) · 5.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
module Stackctl.StackSpec
( StackSpec
, stackSpecFilePath
, stackSpecSpecPath
, stackSpecSpecBody
, stackSpecStackName
, stackSpecStackDescription
, stackSpecDepends
, stackSpecActions
, stackSpecParameters
, stackSpecCapabilities
, stackSpecStackFile
, stackSpecTemplateFile
, stackSpecTags
, buildStackSpec
, TemplateBody
, templateBodyFromValue
, writeStackSpec
, readStackSpec
, createChangeSet
, sortStackSpecs
) where
import Stackctl.Prelude
import qualified CfnFlip
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.Config (HasConfig (..), applyConfig)
import Stackctl.Sort
import Stackctl.StackSpecPath
import Stackctl.StackSpecYaml
import System.FilePath (takeExtension)
import qualified System.FilePath as FilePath
import UnliftIO.Directory (createDirectoryIfMissing, doesFileExist)
data StackSpec = StackSpec
{ ssSpecRoot :: FilePath
, ssSpecPath :: StackSpecPath
, ssSpecBody :: StackSpecYaml
}
stackSpecSpecRoot :: StackSpec -> FilePath
stackSpecSpecRoot = ssSpecRoot
stackSpecFilePath :: StackSpec -> FilePath
stackSpecFilePath spec =
FilePath.normalise $ stackSpecSpecRoot spec </> stackSpecStackFile spec
stackSpecSpecPath :: StackSpec -> StackSpecPath
stackSpecSpecPath = ssSpecPath
stackSpecSpecBody :: StackSpec -> StackSpecYaml
stackSpecSpecBody = ssSpecBody
stackSpecStackName :: StackSpec -> StackName
stackSpecStackName = stackSpecPathStackName . ssSpecPath
stackSpecStackDescription :: StackSpec -> Maybe StackDescription
stackSpecStackDescription = ssyDescription . ssSpecBody
stackSpecDepends :: StackSpec -> [StackName]
stackSpecDepends = fromMaybe [] . ssyDepends . ssSpecBody
stackSpecActions :: StackSpec -> [Action]
stackSpecActions = fromMaybe [] . ssyActions . ssSpecBody
-- | Relative path @stacks/...@
stackSpecStackFile :: StackSpec -> FilePath
stackSpecStackFile = stackSpecPathFilePath . ssSpecPath
-- | Relative path @templates/...@
stackSpecTemplateFile :: StackSpec -> FilePath
stackSpecTemplateFile = ("templates" </>) . ssyTemplate . ssSpecBody
stackSpecTemplate :: StackSpec -> StackTemplate
stackSpecTemplate spec =
StackTemplate
$ FilePath.normalise
$ ssSpecRoot spec </> stackSpecTemplateFile spec
stackSpecParameters :: StackSpec -> [Parameter]
stackSpecParameters =
maybe [] (map unParameterYaml . unParametersYaml) . ssyParameters . ssSpecBody
stackSpecCapabilities :: StackSpec -> [Capability]
stackSpecCapabilities = fromMaybe [] . ssyCapabilities . ssSpecBody
stackSpecTags :: StackSpec -> [Tag]
stackSpecTags = maybe [] (map unTagYaml . unTagsYaml) . ssyTags . ssSpecBody
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
| TemplateJson Value
newtype UnexpectedTemplateJson = UnexpectedTemplateJson
{ _unexpectedTemplateJsonExtension :: String
}
deriving stock (Show)
instance Exception UnexpectedTemplateJson where
displayException (UnexpectedTemplateJson ext) =
"TemplateJson must be written to .yaml or .json, encountered "
<> ext
<> ". To write to an arbitrary path, use TemplateText."
templateBodyFromValue :: Value -> TemplateBody
templateBodyFromValue = \case
String x -> TemplateText x
v -> TemplateJson v
writeTemplateBody :: MonadUnliftIO m => FilePath -> TemplateBody -> m ()
writeTemplateBody path body = do
createDirectoryIfMissing True dir
case (body, ext) of
(TemplateText t, _) -> writeFileUtf8 path t
(TemplateJson v, ".yaml") -> CfnFlip.jsonToYamlFile path v
(TemplateJson v, ".json") -> writeFileBinary path $ BSL.toStrict $ encode v
(TemplateJson _, _) -> throwIO $ UnexpectedTemplateJson ext
where
dir = takeDirectory path
ext = takeExtension path
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 = stackSpecFilePath 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
buildStackSpec dir specPath specBody
where
path = dir </> stackSpecPathFilePath specPath
err e =
throwString $ path <> " is invalid: " <> Yaml.prettyPrintParseException e
-- | Create a Change Set between a Stack Specification and deployed state
createChangeSet
:: ( MonadUnliftIO m
, MonadLogger m
, MonadAWS m
)
=> StackSpec
-> [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)
sortStackSpecs :: [StackSpec] -> [StackSpec]
sortStackSpecs = sortByDependencies stackSpecStackName stackSpecDepends