github.com/kevinklinger/open_terraform@v1.3.6/noninternal/configs/config_build.go (about) 1 package configs 2 3 import ( 4 "sort" 5 6 version "github.com/hashicorp/go-version" 7 "github.com/hashicorp/hcl/v2" 8 "github.com/kevinklinger/open_terraform/noninternal/addrs" 9 ) 10 11 // BuildConfig constructs a Config from a root module by loading all of its 12 // descendent modules via the given ModuleWalker. 13 // 14 // The result is a module tree that has so far only had basic module- and 15 // file-level invariants validated. If the returned diagnostics contains errors, 16 // the returned module tree may be incomplete but can still be used carefully 17 // for static analysis. 18 func BuildConfig(root *Module, walker ModuleWalker) (*Config, hcl.Diagnostics) { 19 var diags hcl.Diagnostics 20 cfg := &Config{ 21 Module: root, 22 } 23 cfg.Root = cfg // Root module is self-referential. 24 cfg.Children, diags = buildChildModules(cfg, walker) 25 26 // Skip provider resolution if there are any errors, since the provider 27 // configurations themselves may not be valid. 28 if !diags.HasErrors() { 29 // Now that the config is built, we can connect the provider names to all 30 // the known types for validation. 31 cfg.resolveProviderTypes() 32 } 33 34 diags = append(diags, validateProviderConfigs(nil, cfg, nil)...) 35 36 return cfg, diags 37 } 38 39 func buildChildModules(parent *Config, walker ModuleWalker) (map[string]*Config, hcl.Diagnostics) { 40 var diags hcl.Diagnostics 41 ret := map[string]*Config{} 42 43 calls := parent.Module.ModuleCalls 44 45 // We'll sort the calls by their local names so that they'll appear in a 46 // predictable order in any logging that's produced during the walk. 47 callNames := make([]string, 0, len(calls)) 48 for k := range calls { 49 callNames = append(callNames, k) 50 } 51 sort.Strings(callNames) 52 53 for _, callName := range callNames { 54 call := calls[callName] 55 path := make([]string, len(parent.Path)+1) 56 copy(path, parent.Path) 57 path[len(path)-1] = call.Name 58 59 req := ModuleRequest{ 60 Name: call.Name, 61 Path: path, 62 SourceAddr: call.SourceAddr, 63 SourceAddrRange: call.SourceAddrRange, 64 VersionConstraint: call.Version, 65 Parent: parent, 66 CallRange: call.DeclRange, 67 } 68 69 mod, ver, modDiags := walker.LoadModule(&req) 70 diags = append(diags, modDiags...) 71 if mod == nil { 72 // nil can be returned if the source address was invalid and so 73 // nothing could be loaded whatsoever. LoadModule should've 74 // returned at least one error diagnostic in that case. 75 continue 76 } 77 78 child := &Config{ 79 Parent: parent, 80 Root: parent.Root, 81 Path: path, 82 Module: mod, 83 CallRange: call.DeclRange, 84 SourceAddr: call.SourceAddr, 85 SourceAddrRange: call.SourceAddrRange, 86 Version: ver, 87 } 88 89 child.Children, modDiags = buildChildModules(child, walker) 90 diags = append(diags, modDiags...) 91 92 if mod.Backend != nil { 93 diags = diags.Append(&hcl.Diagnostic{ 94 Severity: hcl.DiagWarning, 95 Summary: "Backend configuration ignored", 96 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.", 97 Subject: mod.Backend.DeclRange.Ptr(), 98 }) 99 } 100 101 ret[call.Name] = child 102 } 103 104 return ret, diags 105 } 106 107 // A ModuleWalker knows how to find and load a child module given details about 108 // the module to be loaded and a reference to its partially-loaded parent 109 // Config. 110 type ModuleWalker interface { 111 // LoadModule finds and loads a requested child module. 112 // 113 // If errors are detected during loading, implementations should return them 114 // in the diagnostics object. If the diagnostics object contains any errors 115 // then the caller will tolerate the returned module being nil or incomplete. 116 // If no errors are returned, it should be non-nil and complete. 117 // 118 // Full validation need not have been performed but an implementation should 119 // ensure that the basic file- and module-validations performed by the 120 // LoadConfigDir function (valid syntax, no namespace collisions, etc) have 121 // been performed before returning a module. 122 LoadModule(req *ModuleRequest) (*Module, *version.Version, hcl.Diagnostics) 123 } 124 125 // ModuleWalkerFunc is an implementation of ModuleWalker that directly wraps 126 // a callback function, for more convenient use of that interface. 127 type ModuleWalkerFunc func(req *ModuleRequest) (*Module, *version.Version, hcl.Diagnostics) 128 129 // LoadModule implements ModuleWalker. 130 func (f ModuleWalkerFunc) LoadModule(req *ModuleRequest) (*Module, *version.Version, hcl.Diagnostics) { 131 return f(req) 132 } 133 134 // ModuleRequest is used with the ModuleWalker interface to describe a child 135 // module that must be loaded. 136 type ModuleRequest struct { 137 // Name is the "logical name" of the module call within configuration. 138 // This is provided in case the name is used as part of a storage key 139 // for the module, but implementations must otherwise treat it as an 140 // opaque string. It is guaranteed to have already been validated as an 141 // HCL identifier and UTF-8 encoded. 142 Name string 143 144 // Path is a list of logical names that traverse from the root module to 145 // this module. This can be used, for example, to form a lookup key for 146 // each distinct module call in a configuration, allowing for multiple 147 // calls with the same name at different points in the tree. 148 Path addrs.Module 149 150 // SourceAddr is the source address string provided by the user in 151 // configuration. 152 SourceAddr addrs.ModuleSource 153 154 // SourceAddrRange is the source range for the SourceAddr value as it 155 // was provided in configuration. This can and should be used to generate 156 // diagnostics about the source address having invalid syntax, referring 157 // to a non-existent object, etc. 158 SourceAddrRange hcl.Range 159 160 // VersionConstraint is the version constraint applied to the module in 161 // configuration. This data structure includes the source range for 162 // the constraint, which can and should be used to generate diagnostics 163 // about constraint-related issues, such as constraints that eliminate all 164 // available versions of a module whose source is otherwise valid. 165 VersionConstraint VersionConstraint 166 167 // Parent is the partially-constructed module tree node that the loaded 168 // module will be added to. Callers may refer to any field of this 169 // structure except Children, which is still under construction when 170 // ModuleRequest objects are created and thus has undefined content. 171 // The main reason this is provided is so that full module paths can 172 // be constructed for uniqueness. 173 Parent *Config 174 175 // CallRange is the source range for the header of the "module" block 176 // in configuration that prompted this request. This can be used as the 177 // subject of an error diagnostic that relates to the module call itself, 178 // rather than to either its source address or its version number. 179 CallRange hcl.Range 180 } 181 182 // DisabledModuleWalker is a ModuleWalker that doesn't support 183 // child modules at all, and so will return an error if asked to load one. 184 // 185 // This is provided primarily for testing. There is no good reason to use this 186 // in the main application. 187 var DisabledModuleWalker ModuleWalker 188 189 func init() { 190 DisabledModuleWalker = ModuleWalkerFunc(func(req *ModuleRequest) (*Module, *version.Version, hcl.Diagnostics) { 191 return nil, nil, hcl.Diagnostics{ 192 { 193 Severity: hcl.DiagError, 194 Summary: "Child modules are not supported", 195 Detail: "Child module calls are not allowed in this context.", 196 Subject: &req.CallRange, 197 }, 198 } 199 }) 200 }