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