forked from gruntwork-io/terragrunt
-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathmodule.go
358 lines (310 loc) · 14.9 KB
/
module.go
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
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
package configstack
import (
"fmt"
"path/filepath"
"strings"
"github.com/coveooss/gotemplate/v3/collections"
"github.com/coveooss/gotemplate/v3/utils"
"github.com/coveooss/terragrunt/v2/config"
"github.com/coveooss/terragrunt/v2/options"
"github.com/coveooss/terragrunt/v2/shell"
"github.com/coveooss/terragrunt/v2/tgerrors"
"github.com/coveooss/terragrunt/v2/util"
)
// TerraformModule represents a single module (i.e. folder with Terraform templates), including the Terragrunt configuration for that
// module and the list of other modules that this module depends on
type TerraformModule struct {
Path string
Dependencies []*TerraformModule
Config config.TerragruntConfig
TerragruntOptions *options.TerragruntOptions
AssumeAlreadyApplied bool
}
// Render this module as a human-readable string
func (module TerraformModule) String() string {
return fmt.Sprintf("Module %s (dependencies: [%s])", util.GetPathRelativeToWorkingDirMax(module.Path, 3), strings.Join(module.dependencies(), ", "))
}
// Run a module once all of its dependencies have finished executing.
func (module TerraformModule) dependencies() []string {
result := make([]string, 0, len(module.Dependencies))
for _, dep := range module.Dependencies {
result = append(result, util.GetPathRelativeToWorkingDirMax(dep.Path, 3))
}
return result
}
// Simple returns a simplified version of the module with paths relative to working dir
func (module *TerraformModule) Simple() SimpleTerraformModule {
dependencies := []string{}
for _, dependency := range module.Dependencies {
dependencies = append(dependencies, dependency.Path)
}
return SimpleTerraformModule{module.Path, dependencies}
}
// SimpleTerraformModule represents a simplified version of TerraformModule
type SimpleTerraformModule struct {
Path string `json:"path"`
Dependencies []string `json:"dependencies,omitempty" yaml:",omitempty"`
}
// SimpleTerraformModules represents a list of simplified version of TerraformModule
type SimpleTerraformModules []SimpleTerraformModule
// MakeRelative transforms each absolute path in relative path
func (modules SimpleTerraformModules) MakeRelative() (result SimpleTerraformModules) {
result = make(SimpleTerraformModules, len(modules))
for i := range modules {
result[i].Path = util.GetPathRelativeToWorkingDir(modules[i].Path)
result[i].Dependencies = make([]string, len(modules[i].Dependencies))
for j := range result[i].Dependencies {
result[i].Dependencies[j] = util.GetPathRelativeToWorkingDir(modules[i].Dependencies[j])
}
}
return
}
// ResolveTerraformModules goes through each of the given Terragrunt configuration files and resolve the module that configuration file represents
// into a TerraformModule struct. Return the list of these TerraformModule structures.
func ResolveTerraformModules(terragruntConfigPaths []string, terragruntOptions *options.TerragruntOptions) ([]*TerraformModule, error) {
canonicalTerragruntConfigPaths, err := util.CanonicalPaths(terragruntConfigPaths, ".")
if err != nil {
return []*TerraformModule{}, err
}
modules, err := resolveModules(canonicalTerragruntConfigPaths, terragruntOptions, false)
if err != nil {
return []*TerraformModule{}, err
}
// We remove any path that are not in the resolved module
if terragruntOptions.CheckSourceFolders {
canonicalTerragruntConfigPaths = func() []string {
filtered := make([]string, 0, len(canonicalTerragruntConfigPaths))
for _, path := range canonicalTerragruntConfigPaths {
if modules[path] != nil {
filtered = append(filtered, path)
}
}
return filtered
}()
}
externalDependencies, err := resolveExternalDependenciesForModules(canonicalTerragruntConfigPaths, modules, terragruntOptions)
if err != nil {
return []*TerraformModule{}, err
}
return crossLinkDependencies(mergeMaps(modules, externalDependencies), canonicalTerragruntConfigPaths)
}
// Go through each of the given Terragrunt configuration files and resolve the module that configuration file represents
// into a TerraformModule struct. Note that this method will NOT fill in the Dependencies field of the TerraformModule
// struct (see the crossLinkDependencies method for that). Return a map from module path to TerraformModule struct.
//
// resolveExternal is used to exclude modules that don't contain terraform files. This is used to avoid requirements of
// adding terragrunt.ignore when a parent folder doesn't have terraform files to deploy by itself.
func resolveModules(canonicalTerragruntConfigPaths []string, terragruntOptions *options.TerragruntOptions, resolveExternal bool) (map[string]*TerraformModule, error) {
moduleMap := map[string]*TerraformModule{}
for _, terragruntConfigPath := range canonicalTerragruntConfigPaths {
if module, tfFiles, err := resolveTerraformModule(terragruntConfigPath, terragruntOptions); err == nil {
if resolveExternal && module != nil || tfFiles {
moduleMap[module.Path] = module
}
} else {
return moduleMap, err
}
}
return moduleMap, nil
}
// Create a TerraformModule struct for the Terraform module specified by the given Terragrunt configuration file path.
// Note that this method will NOT fill in the Dependencies field of the TerraformModule struct (see the
// crossLinkDependencies method for that).
func resolveTerraformModule(terragruntConfigPath string, terragruntOptions *options.TerragruntOptions) (module *TerraformModule, tfFiles bool, err error) {
modulePath, err := util.CanonicalPath(filepath.Dir(terragruntConfigPath), ".")
if err != nil {
return
}
opts := terragruntOptions.Clone(terragruntConfigPath)
_, terragruntConfig, err := config.ParseConfigFile(opts, config.IncludeConfig{Path: terragruntConfigPath})
if err != nil {
return
}
// Fix for https://github.com/gruntwork-io/terragrunt/issues/208
matches, err := utils.FindFiles(filepath.Dir(terragruntConfigPath), false, false, options.TerraformFilesTemplates...)
if err != nil {
return
}
if matches == nil {
if terragruntConfig.Terraform == nil || terragruntConfig.Terraform.Source == "" {
terragruntOptions.Logger.Debugf("Module %s does not have an associated terraform configuration and will be skipped.", filepath.Dir(terragruntConfigPath))
return
}
if terragruntOptions.CheckSourceFolders {
sourcePath := terragruntConfig.Terraform.Source
if !filepath.IsAbs(sourcePath) {
sourcePath, _ = util.CanonicalPath(sourcePath, filepath.Dir(terragruntConfigPath))
}
matches, err = utils.FindFiles(sourcePath, false, false, options.TerraformFilesTemplates...)
}
}
tfFiles = len(matches) > 0 || !terragruntOptions.CheckSourceFolders
module = &TerraformModule{Path: modulePath, Config: *terragruntConfig, TerragruntOptions: opts}
return
}
// Look through the dependencies of the modules in the given map and resolve the "external" dependency paths listed in
// each modules config (i.e. those dependencies not in the given list of Terragrunt config canonical file paths).
// These external dependencies are outside of the current working directory, which means they may not be part of the
// environment the user is trying to apply-all or destroy-all. Therefore, this method also confirms whether the user wants
// to actually apply those dependencies or just assume they are already applied. Note that this method will NOT fill in
// the Dependencies field of the TerraformModule struct (see the crossLinkDependencies method for that).
func resolveExternalDependenciesForModules(canonicalTerragruntConfigPaths []string, moduleMap map[string]*TerraformModule, terragruntOptions *options.TerragruntOptions) (map[string]*TerraformModule, error) {
allExternalDependencies := map[string]*TerraformModule{}
for _, module := range moduleMap {
externalDependencies, err := resolveExternalDependenciesForModule(module, canonicalTerragruntConfigPaths, terragruntOptions)
if err != nil {
return externalDependencies, err
}
for _, externalDependency := range externalDependencies {
if _, alreadyFound := moduleMap[externalDependency.Path]; alreadyFound {
continue
}
var expandDependencies []*TerraformModule
for key, existingModule := range moduleMap {
if strings.HasPrefix(key, externalDependency.Path) {
expandDependencies = append(expandDependencies, existingModule)
}
}
if len(expandDependencies) > 0 {
module.Dependencies = append(module.Dependencies, expandDependencies...)
subFoldersDependencies := make([]string, len(expandDependencies))
for i := range expandDependencies {
subFoldersDependencies[i] = expandDependencies[i].Path
}
module.Config.Dependencies.Paths = util.RemoveElementFromList(module.Config.Dependencies.Paths, externalDependency.Path)
module.Config.Dependencies.Paths = append(module.Config.Dependencies.Paths, subFoldersDependencies...)
continue
}
alreadyApplied, err := confirmExternalDependencyAlreadyApplied(module, externalDependency, terragruntOptions)
if err != nil {
return externalDependencies, err
}
externalDependency.AssumeAlreadyApplied = alreadyApplied
allExternalDependencies[externalDependency.Path] = externalDependency
}
}
return allExternalDependencies, nil
}
// Look through the dependencies of the given module and resolve the "external" dependency paths listed in the module's
// config (i.e. those dependencies not in the given list of Terragrunt config canonical file paths). These external
// dependencies are outside of the current working directory, which means they may not be part of the environment the
// user is trying to apply-all or destroy-all. Note that this method will NOT fill in the Dependencies field of the
// TerraformModule struct (see the crossLinkDependencies method for that).
func resolveExternalDependenciesForModule(module *TerraformModule, canonicalTerragruntConfigPaths []string, terragruntOptions *options.TerragruntOptions) (result map[string]*TerraformModule, err error) {
result = make(map[string]*TerraformModule)
if module.Config.Dependencies == nil || len(module.Config.Dependencies.Paths) == 0 {
return
}
externalTerragruntConfigPaths := []string{}
for _, dependency := range module.Config.Dependencies.Paths {
var dependencyPath string
if dependencyPath, err = util.CanonicalPath(dependency, module.Path); err != nil {
return
}
var configs []string
if terragruntConfigPath, exists := terragruntOptions.ConfigPath(dependencyPath); exists {
configs = append(configs, terragruntConfigPath)
} else if util.FileExists(dependencyPath) {
if configs, err = terragruntOptions.FindConfigFilesInPath(dependencyPath); err != nil {
return
}
}
for _, config := range configs {
if !util.ListContainsElement(canonicalTerragruntConfigPaths, config) {
externalTerragruntConfigPaths = append(externalTerragruntConfigPaths, config)
}
}
}
return resolveModules(externalTerragruntConfigPaths, terragruntOptions, true)
}
// Confirm with the user whether they want Terragrunt to assume the given dependency of the given module is already
// applied. If the user selects "no", then Terragrunt will apply that module as well.
func confirmExternalDependencyAlreadyApplied(module *TerraformModule, dependency *TerraformModule, terragruntOptions *options.TerragruntOptions) (bool, error) {
prompt := fmt.Sprintf("Module %s depends on module %s, which is an external dependency outside of the current working directory. "+
"Should Terragrunt skip over this external dependency? Warning, if you say 'no', Terragrunt will make changes in %s as well!",
module.Path, dependency.Path, dependency.Path)
return shell.PromptUserForYesNo(prompt, terragruntOptions)
}
// Merge the given external dependencies into the given map of modules if those dependencies aren't already in the
// modules map
func mergeMaps(modules map[string]*TerraformModule, externalDependencies map[string]*TerraformModule) map[string]*TerraformModule {
out := map[string]*TerraformModule{}
for key, value := range externalDependencies {
out[key] = value
}
for key, value := range modules {
out[key] = value
}
return out
}
// Go through each module in the given map and cross-link its dependencies to the other modules in that same map. If
// a dependency is referenced that is not in the given map, return an error.
func crossLinkDependencies(moduleMap map[string]*TerraformModule, canonicalTerragruntConfigPaths []string) ([]*TerraformModule, error) {
modules := []*TerraformModule{}
for _, module := range moduleMap {
dependencies, err := getDependenciesForModule(module, moduleMap, canonicalTerragruntConfigPaths)
if err != nil {
return modules, err
}
module.Dependencies = dependencies
modules = append(modules, module)
}
return modules, nil
}
// Get the list of modules this module depends on
func getDependenciesForModule(module *TerraformModule, moduleMap map[string]*TerraformModule, terragruntConfigPaths []string) ([]*TerraformModule, error) {
dependencies := make([]*TerraformModule, 0, len(moduleMap))
dependenciesPaths := make([]string, 0, len(moduleMap))
if module.Config.Dependencies == nil || len(module.Config.Dependencies.Paths) == 0 {
return dependencies, nil
}
for _, dependencyPath := range module.Config.Dependencies.Paths {
if module.Path == dependencyPath {
continue
}
dependencyModulePath, err := util.CanonicalPath(dependencyPath, module.Path)
if err != nil {
return dependencies, nil
}
if dependencyModule, foundModule := moduleMap[dependencyModulePath]; foundModule {
if !util.ListContainsElement(dependenciesPaths, dependencyModulePath) {
// We avoid adding the same module dependency more than once
dependenciesPaths = append(dependenciesPaths, dependencyModulePath)
dependencies = append(dependencies, dependencyModule)
}
} else {
var foundModules []*TerraformModule
// The dependency may be a parent folder
for _, key := range collections.AsDictionary(moduleMap).KeysAsString() {
if key.HasPrefix(dependencyModulePath + "/") {
foundModule = true
if !util.ListContainsElement(dependenciesPaths, key.Str()) {
// We avoid adding the same module dependency more than once
dependenciesPaths = append(dependenciesPaths, key.Str())
foundModules = append(foundModules, moduleMap[key.Str()])
}
}
}
if !foundModule && !module.AssumeAlreadyApplied {
err := UnrecognizedDependency{
ModulePath: module.Path,
DependencyPath: dependencyPath,
TerragruntConfigPaths: terragruntConfigPaths,
}
return dependencies, tgerrors.WithStackTrace(err)
}
dependencies = append(dependencies, foundModules...)
}
}
return dependencies, nil
}
// Custom error types
// UnrecognizedDependency describes error when a dependency cannot be resolved
type UnrecognizedDependency struct {
ModulePath string
DependencyPath string
TerragruntConfigPaths []string
}
func (err UnrecognizedDependency) Error() string {
return fmt.Sprintf("Module %s specifies %s as a dependency, but that dependency was not one of the ones found while scanning subfolders: %v", err.ModulePath, err.DependencyPath, err.TerragruntConfigPaths)
}