github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/configs/config_build.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package configs 5 6 import ( 7 "fmt" 8 "path" 9 "sort" 10 "strings" 11 12 version "github.com/hashicorp/go-version" 13 "github.com/hashicorp/hcl/v2" 14 15 "github.com/terramate-io/tf/addrs" 16 ) 17 18 // BuildConfig constructs a Config from a root module by loading all of its 19 // descendent modules via the given ModuleWalker. 20 // 21 // The result is a module tree that has so far only had basic module- and 22 // file-level invariants validated. If the returned diagnostics contains errors, 23 // the returned module tree may be incomplete but can still be used carefully 24 // for static analysis. 25 func BuildConfig(root *Module, walker ModuleWalker) (*Config, hcl.Diagnostics) { 26 var diags hcl.Diagnostics 27 cfg := &Config{ 28 Module: root, 29 } 30 cfg.Root = cfg // Root module is self-referential. 31 cfg.Children, diags = buildChildModules(cfg, walker) 32 diags = append(diags, buildTestModules(cfg, walker)...) 33 34 // Skip provider resolution if there are any errors, since the provider 35 // configurations themselves may not be valid. 36 if !diags.HasErrors() { 37 // Now that the config is built, we can connect the provider names to all 38 // the known types for validation. 39 providers := cfg.resolveProviderTypes() 40 cfg.resolveProviderTypesForTests(providers) 41 } 42 43 diags = append(diags, validateProviderConfigs(nil, cfg, nil)...) 44 diags = append(diags, validateProviderConfigsForTests(cfg)...) 45 46 return cfg, diags 47 } 48 49 func buildTestModules(root *Config, walker ModuleWalker) hcl.Diagnostics { 50 var diags hcl.Diagnostics 51 52 for name, file := range root.Module.Tests { 53 for _, run := range file.Runs { 54 if run.Module == nil { 55 continue 56 } 57 58 // We want to make sure the path for the testing modules are unique 59 // so we create a dedicated path for them. 60 // 61 // Some examples: 62 // - file: main.tftest.hcl, run: setup - test.main.setup 63 // - file: tests/main.tftest.hcl, run: setup - test.tests.main.setup 64 65 dir := path.Dir(name) 66 base := path.Base(name) 67 68 path := addrs.Module{} 69 path = append(path, "test") 70 if dir != "." { 71 path = append(path, strings.Split(dir, "/")...) 72 } 73 path = append(path, strings.TrimSuffix(base, ".tftest.hcl"), run.Name) 74 75 req := ModuleRequest{ 76 Name: run.Name, 77 Path: path, 78 SourceAddr: run.Module.Source, 79 SourceAddrRange: run.Module.SourceDeclRange, 80 VersionConstraint: run.Module.Version, 81 Parent: root, 82 CallRange: run.Module.DeclRange, 83 } 84 85 cfg, modDiags := loadModule(root, &req, walker) 86 diags = append(diags, modDiags...) 87 88 if cfg != nil { 89 // To get the loader to work, we need to set a bunch of values 90 // (like the name, path, and parent) as if the module was being 91 // loaded as a child of the root config. 92 // 93 // In actuality, when this is executed it will be as if the 94 // module was the root. So, we'll post-process some things to 95 // get it to behave as expected later. 96 97 // First, update the main module for this test run to behave as 98 // if it is the root module. 99 cfg.Parent = nil 100 101 // Then we need to update the paths for this config and all 102 // children, so they think they are all relative to the root 103 // module we just created. 104 rebaseChildModule(cfg, cfg) 105 106 // Finally, link the new config back into our test run so 107 // it can be retrieved later. 108 run.ConfigUnderTest = cfg 109 } 110 } 111 } 112 113 return diags 114 } 115 116 func buildChildModules(parent *Config, walker ModuleWalker) (map[string]*Config, hcl.Diagnostics) { 117 var diags hcl.Diagnostics 118 ret := map[string]*Config{} 119 120 calls := parent.Module.ModuleCalls 121 122 // We'll sort the calls by their local names so that they'll appear in a 123 // predictable order in any logging that's produced during the walk. 124 callNames := make([]string, 0, len(calls)) 125 for k := range calls { 126 callNames = append(callNames, k) 127 } 128 sort.Strings(callNames) 129 130 for _, callName := range callNames { 131 call := calls[callName] 132 path := make([]string, len(parent.Path)+1) 133 copy(path, parent.Path) 134 path[len(path)-1] = call.Name 135 136 req := ModuleRequest{ 137 Name: call.Name, 138 Path: path, 139 SourceAddr: call.SourceAddr, 140 SourceAddrRange: call.SourceAddrRange, 141 VersionConstraint: call.Version, 142 Parent: parent, 143 CallRange: call.DeclRange, 144 } 145 child, modDiags := loadModule(parent.Root, &req, walker) 146 diags = append(diags, modDiags...) 147 if child == nil { 148 // This means an error occurred, there should be diagnostics within 149 // modDiags for this. 150 continue 151 } 152 153 ret[call.Name] = child 154 } 155 156 return ret, diags 157 } 158 159 func loadModule(root *Config, req *ModuleRequest, walker ModuleWalker) (*Config, hcl.Diagnostics) { 160 var diags hcl.Diagnostics 161 162 mod, ver, modDiags := walker.LoadModule(req) 163 diags = append(diags, modDiags...) 164 if mod == nil { 165 // nil can be returned if the source address was invalid and so 166 // nothing could be loaded whatsoever. LoadModule should've 167 // returned at least one error diagnostic in that case. 168 return nil, diags 169 } 170 171 cfg := &Config{ 172 Parent: req.Parent, 173 Root: root, 174 Path: req.Path, 175 Module: mod, 176 CallRange: req.CallRange, 177 SourceAddr: req.SourceAddr, 178 SourceAddrRange: req.SourceAddrRange, 179 Version: ver, 180 } 181 182 cfg.Children, modDiags = buildChildModules(cfg, walker) 183 diags = append(diags, modDiags...) 184 185 if mod.Backend != nil { 186 diags = diags.Append(&hcl.Diagnostic{ 187 Severity: hcl.DiagWarning, 188 Summary: "Backend configuration ignored", 189 Detail: "Any selected backend applies to the entire configuration, so Terraform expects provider configurations only in the root module.\n\nThis is a warning rather than an error because it's sometimes convenient to temporarily call a root module as a child module for testing purposes, but this backend configuration block will have no effect.", 190 Subject: mod.Backend.DeclRange.Ptr(), 191 }) 192 } 193 194 if len(mod.Import) > 0 { 195 diags = diags.Append(&hcl.Diagnostic{ 196 Severity: hcl.DiagError, 197 Summary: "Invalid import configuration", 198 Detail: fmt.Sprintf("An import block was detected in %q. Import blocks are only allowed in the root module.", cfg.Path), 199 Subject: mod.Import[0].DeclRange.Ptr(), 200 }) 201 } 202 203 return cfg, diags 204 } 205 206 // rebaseChildModule updates cfg to make it act as if root is the base of the 207 // module tree. 208 // 209 // This is used for modules loaded directly from test files. In order to load 210 // them properly, and reuse the code for loading modules from normal 211 // configuration files, we pretend they are children of the main configuration 212 // object. Later, when it comes time for them to execute they will act as if 213 // they are the root module directly. 214 // 215 // This function updates cfg so that it treats the provided root as the actual 216 // root of this module tree. It then recurses into all the child modules and 217 // does the same for them. 218 func rebaseChildModule(cfg *Config, root *Config) { 219 for _, child := range cfg.Children { 220 rebaseChildModule(child, root) 221 } 222 223 cfg.Path = cfg.Path[len(root.Path):] 224 cfg.Root = root 225 } 226 227 // A ModuleWalker knows how to find and load a child module given details about 228 // the module to be loaded and a reference to its partially-loaded parent 229 // Config. 230 type ModuleWalker interface { 231 // LoadModule finds and loads a requested child module. 232 // 233 // If errors are detected during loading, implementations should return them 234 // in the diagnostics object. If the diagnostics object contains any errors 235 // then the caller will tolerate the returned module being nil or incomplete. 236 // If no errors are returned, it should be non-nil and complete. 237 // 238 // Full validation need not have been performed but an implementation should 239 // ensure that the basic file- and module-validations performed by the 240 // LoadConfigDir function (valid syntax, no namespace collisions, etc) have 241 // been performed before returning a module. 242 LoadModule(req *ModuleRequest) (*Module, *version.Version, hcl.Diagnostics) 243 } 244 245 // ModuleWalkerFunc is an implementation of ModuleWalker that directly wraps 246 // a callback function, for more convenient use of that interface. 247 type ModuleWalkerFunc func(req *ModuleRequest) (*Module, *version.Version, hcl.Diagnostics) 248 249 // LoadModule implements ModuleWalker. 250 func (f ModuleWalkerFunc) LoadModule(req *ModuleRequest) (*Module, *version.Version, hcl.Diagnostics) { 251 return f(req) 252 } 253 254 // ModuleRequest is used with the ModuleWalker interface to describe a child 255 // module that must be loaded. 256 type ModuleRequest struct { 257 // Name is the "logical name" of the module call within configuration. 258 // This is provided in case the name is used as part of a storage key 259 // for the module, but implementations must otherwise treat it as an 260 // opaque string. It is guaranteed to have already been validated as an 261 // HCL identifier and UTF-8 encoded. 262 Name string 263 264 // Path is a list of logical names that traverse from the root module to 265 // this module. This can be used, for example, to form a lookup key for 266 // each distinct module call in a configuration, allowing for multiple 267 // calls with the same name at different points in the tree. 268 Path addrs.Module 269 270 // SourceAddr is the source address string provided by the user in 271 // configuration. 272 SourceAddr addrs.ModuleSource 273 274 // SourceAddrRange is the source range for the SourceAddr value as it 275 // was provided in configuration. This can and should be used to generate 276 // diagnostics about the source address having invalid syntax, referring 277 // to a non-existent object, etc. 278 SourceAddrRange hcl.Range 279 280 // VersionConstraint is the version constraint applied to the module in 281 // configuration. This data structure includes the source range for 282 // the constraint, which can and should be used to generate diagnostics 283 // about constraint-related issues, such as constraints that eliminate all 284 // available versions of a module whose source is otherwise valid. 285 VersionConstraint VersionConstraint 286 287 // Parent is the partially-constructed module tree node that the loaded 288 // module will be added to. Callers may refer to any field of this 289 // structure except Children, which is still under construction when 290 // ModuleRequest objects are created and thus has undefined content. 291 // The main reason this is provided is so that full module paths can 292 // be constructed for uniqueness. 293 Parent *Config 294 295 // CallRange is the source range for the header of the "module" block 296 // in configuration that prompted this request. This can be used as the 297 // subject of an error diagnostic that relates to the module call itself, 298 // rather than to either its source address or its version number. 299 CallRange hcl.Range 300 } 301 302 // DisabledModuleWalker is a ModuleWalker that doesn't support 303 // child modules at all, and so will return an error if asked to load one. 304 // 305 // This is provided primarily for testing. There is no good reason to use this 306 // in the main application. 307 var DisabledModuleWalker ModuleWalker 308 309 func init() { 310 DisabledModuleWalker = ModuleWalkerFunc(func(req *ModuleRequest) (*Module, *version.Version, hcl.Diagnostics) { 311 return nil, nil, hcl.Diagnostics{ 312 { 313 Severity: hcl.DiagError, 314 Summary: "Child modules are not supported", 315 Detail: "Child module calls are not allowed in this context.", 316 Subject: &req.CallRange, 317 }, 318 } 319 }) 320 }