github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/configs/parser_config.go (about) 1 package configs 2 3 import ( 4 "github.com/hashicorp/hcl/v2" 5 ) 6 7 // LoadConfigFile reads the file at the given path and parses it as a config 8 // file. 9 // 10 // If the file cannot be read -- for example, if it does not exist -- then 11 // a nil *File will be returned along with error diagnostics. Callers may wish 12 // to disregard the returned diagnostics in this case and instead generate 13 // their own error message(s) with additional context. 14 // 15 // If the returned diagnostics has errors when a non-nil map is returned 16 // then the map may be incomplete but should be valid enough for careful 17 // static analysis. 18 // 19 // This method wraps LoadHCLFile, and so it inherits the syntax selection 20 // behaviors documented for that method. 21 func (p *Parser) LoadConfigFile(path string) (*File, hcl.Diagnostics) { 22 return p.loadConfigFile(path, false) 23 } 24 25 // LoadConfigFileOverride is the same as LoadConfigFile except that it relaxes 26 // certain required attribute constraints in order to interpret the given 27 // file as an overrides file. 28 func (p *Parser) LoadConfigFileOverride(path string) (*File, hcl.Diagnostics) { 29 return p.loadConfigFile(path, true) 30 } 31 32 func (p *Parser) loadConfigFile(path string, override bool) (*File, hcl.Diagnostics) { 33 34 body, diags := p.LoadHCLFile(path) 35 if body == nil { 36 return nil, diags 37 } 38 39 file := &File{} 40 41 var reqDiags hcl.Diagnostics 42 file.CoreVersionConstraints, reqDiags = sniffCoreVersionRequirements(body) 43 diags = append(diags, reqDiags...) 44 45 // We'll load the experiments first because other decoding logic in the 46 // loop below might depend on these experiments. 47 var expDiags hcl.Diagnostics 48 file.ActiveExperiments, expDiags = sniffActiveExperiments(body) 49 diags = append(diags, expDiags...) 50 51 content, contentDiags := body.Content(configFileSchema) 52 diags = append(diags, contentDiags...) 53 54 for _, block := range content.Blocks { 55 switch block.Type { 56 57 case "terraform": 58 content, contentDiags := block.Body.Content(terraformBlockSchema) 59 diags = append(diags, contentDiags...) 60 61 // We ignore the "terraform_version" and "experiments" attributes 62 // here because sniffCoreVersionRequirements and 63 // sniffActiveExperiments already dealt with those above. 64 65 for _, innerBlock := range content.Blocks { 66 switch innerBlock.Type { 67 68 case "backend": 69 backendCfg, cfgDiags := decodeBackendBlock(innerBlock) 70 diags = append(diags, cfgDiags...) 71 if backendCfg != nil { 72 file.Backends = append(file.Backends, backendCfg) 73 } 74 75 case "required_providers": 76 reqs, reqsDiags := decodeRequiredProvidersBlock(innerBlock) 77 diags = append(diags, reqsDiags...) 78 file.RequiredProviders = append(file.RequiredProviders, reqs...) 79 80 default: 81 // Should never happen because the above cases should be exhaustive 82 // for all block type names in our schema. 83 continue 84 85 } 86 } 87 88 case "provider": 89 cfg, cfgDiags := decodeProviderBlock(block) 90 diags = append(diags, cfgDiags...) 91 if cfg != nil { 92 file.ProviderConfigs = append(file.ProviderConfigs, cfg) 93 } 94 95 case "variable": 96 cfg, cfgDiags := decodeVariableBlock(block, override) 97 diags = append(diags, cfgDiags...) 98 if cfg != nil { 99 file.Variables = append(file.Variables, cfg) 100 } 101 102 case "locals": 103 defs, defsDiags := decodeLocalsBlock(block) 104 diags = append(diags, defsDiags...) 105 file.Locals = append(file.Locals, defs...) 106 107 case "output": 108 cfg, cfgDiags := decodeOutputBlock(block, override) 109 diags = append(diags, cfgDiags...) 110 if cfg != nil { 111 file.Outputs = append(file.Outputs, cfg) 112 } 113 114 case "module": 115 cfg, cfgDiags := decodeModuleBlock(block, override) 116 diags = append(diags, cfgDiags...) 117 if cfg != nil { 118 file.ModuleCalls = append(file.ModuleCalls, cfg) 119 } 120 121 case "resource": 122 cfg, cfgDiags := decodeResourceBlock(block) 123 diags = append(diags, cfgDiags...) 124 if cfg != nil { 125 file.ManagedResources = append(file.ManagedResources, cfg) 126 } 127 128 case "data": 129 cfg, cfgDiags := decodeDataBlock(block) 130 diags = append(diags, cfgDiags...) 131 if cfg != nil { 132 file.DataResources = append(file.DataResources, cfg) 133 } 134 135 default: 136 // Should never happen because the above cases should be exhaustive 137 // for all block type names in our schema. 138 continue 139 140 } 141 } 142 143 return file, diags 144 } 145 146 // sniffCoreVersionRequirements does minimal parsing of the given body for 147 // "terraform" blocks with "required_version" attributes, returning the 148 // requirements found. 149 // 150 // This is intended to maximize the chance that we'll be able to read the 151 // requirements (syntax errors notwithstanding) even if the config file contains 152 // constructs that might've been added in future Terraform versions 153 // 154 // This is a "best effort" sort of method which will return constraints it is 155 // able to find, but may return no constraints at all if the given body is 156 // so invalid that it cannot be decoded at all. 157 func sniffCoreVersionRequirements(body hcl.Body) ([]VersionConstraint, hcl.Diagnostics) { 158 rootContent, _, diags := body.PartialContent(configFileTerraformBlockSniffRootSchema) 159 160 var constraints []VersionConstraint 161 162 for _, block := range rootContent.Blocks { 163 content, _, blockDiags := block.Body.PartialContent(configFileVersionSniffBlockSchema) 164 diags = append(diags, blockDiags...) 165 166 attr, exists := content.Attributes["required_version"] 167 if !exists { 168 continue 169 } 170 171 constraint, constraintDiags := decodeVersionConstraint(attr) 172 diags = append(diags, constraintDiags...) 173 if !constraintDiags.HasErrors() { 174 constraints = append(constraints, constraint) 175 } 176 } 177 178 return constraints, diags 179 } 180 181 // configFileSchema is the schema for the top-level of a config file. We use 182 // the low-level HCL API for this level so we can easily deal with each 183 // block type separately with its own decoding logic. 184 var configFileSchema = &hcl.BodySchema{ 185 Blocks: []hcl.BlockHeaderSchema{ 186 { 187 Type: "terraform", 188 }, 189 { 190 Type: "provider", 191 LabelNames: []string{"name"}, 192 }, 193 { 194 Type: "variable", 195 LabelNames: []string{"name"}, 196 }, 197 { 198 Type: "locals", 199 }, 200 { 201 Type: "output", 202 LabelNames: []string{"name"}, 203 }, 204 { 205 Type: "module", 206 LabelNames: []string{"name"}, 207 }, 208 { 209 Type: "resource", 210 LabelNames: []string{"type", "name"}, 211 }, 212 { 213 Type: "data", 214 LabelNames: []string{"type", "name"}, 215 }, 216 }, 217 } 218 219 // terraformBlockSchema is the schema for a top-level "terraform" block in 220 // a configuration file. 221 var terraformBlockSchema = &hcl.BodySchema{ 222 Attributes: []hcl.AttributeSchema{ 223 {Name: "required_version"}, 224 {Name: "experiments"}, 225 }, 226 Blocks: []hcl.BlockHeaderSchema{ 227 { 228 Type: "backend", 229 LabelNames: []string{"type"}, 230 }, 231 { 232 Type: "required_providers", 233 }, 234 }, 235 } 236 237 // configFileTerraformBlockSniffRootSchema is a schema for 238 // sniffCoreVersionRequirements and sniffActiveExperiments. 239 var configFileTerraformBlockSniffRootSchema = &hcl.BodySchema{ 240 Blocks: []hcl.BlockHeaderSchema{ 241 { 242 Type: "terraform", 243 }, 244 }, 245 } 246 247 // configFileVersionSniffBlockSchema is a schema for sniffCoreVersionRequirements 248 var configFileVersionSniffBlockSchema = &hcl.BodySchema{ 249 Attributes: []hcl.AttributeSchema{ 250 { 251 Name: "required_version", 252 }, 253 }, 254 } 255 256 // configFileExperimentsSniffBlockSchema is a schema for sniffActiveExperiments, 257 // to decode a single attribute from inside a "terraform" block. 258 var configFileExperimentsSniffBlockSchema = &hcl.BodySchema{ 259 Attributes: []hcl.AttributeSchema{ 260 { 261 Name: "experiments", 262 }, 263 }, 264 }