kubeform.dev/terraform-backend-sdk@v0.0.0-20220310143633-45f07fe731c5/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", "language" and "experiments" 62 // attributes 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 case "provider_meta": 81 providerCfg, cfgDiags := decodeProviderMetaBlock(innerBlock) 82 diags = append(diags, cfgDiags...) 83 if providerCfg != nil { 84 file.ProviderMetas = append(file.ProviderMetas, providerCfg) 85 } 86 87 default: 88 // Should never happen because the above cases should be exhaustive 89 // for all block type names in our schema. 90 continue 91 92 } 93 } 94 95 case "required_providers": 96 // required_providers should be nested inside a "terraform" block 97 diags = append(diags, &hcl.Diagnostic{ 98 Severity: hcl.DiagError, 99 Summary: "Invalid required_providers block", 100 Detail: "A \"required_providers\" block must be nested inside a \"terraform\" block.", 101 Subject: block.TypeRange.Ptr(), 102 }) 103 104 case "provider": 105 cfg, cfgDiags := decodeProviderBlock(block) 106 diags = append(diags, cfgDiags...) 107 if cfg != nil { 108 file.ProviderConfigs = append(file.ProviderConfigs, cfg) 109 } 110 111 case "variable": 112 cfg, cfgDiags := decodeVariableBlock(block, override) 113 diags = append(diags, cfgDiags...) 114 if cfg != nil { 115 file.Variables = append(file.Variables, cfg) 116 } 117 118 case "locals": 119 defs, defsDiags := decodeLocalsBlock(block) 120 diags = append(diags, defsDiags...) 121 file.Locals = append(file.Locals, defs...) 122 123 case "output": 124 cfg, cfgDiags := decodeOutputBlock(block, override) 125 diags = append(diags, cfgDiags...) 126 if cfg != nil { 127 file.Outputs = append(file.Outputs, cfg) 128 } 129 130 case "module": 131 cfg, cfgDiags := decodeModuleBlock(block, override) 132 diags = append(diags, cfgDiags...) 133 if cfg != nil { 134 file.ModuleCalls = append(file.ModuleCalls, cfg) 135 } 136 137 case "resource": 138 cfg, cfgDiags := decodeResourceBlock(block) 139 diags = append(diags, cfgDiags...) 140 if cfg != nil { 141 file.ManagedResources = append(file.ManagedResources, cfg) 142 } 143 144 case "data": 145 cfg, cfgDiags := decodeDataBlock(block) 146 diags = append(diags, cfgDiags...) 147 if cfg != nil { 148 file.DataResources = append(file.DataResources, cfg) 149 } 150 151 case "moved": 152 cfg, cfgDiags := decodeMovedBlock(block) 153 diags = append(diags, cfgDiags...) 154 if cfg != nil { 155 file.Moved = append(file.Moved, cfg) 156 } 157 158 default: 159 // Should never happen because the above cases should be exhaustive 160 // for all block type names in our schema. 161 continue 162 163 } 164 } 165 166 return file, diags 167 } 168 169 // sniffCoreVersionRequirements does minimal parsing of the given body for 170 // "terraform" blocks with "required_version" attributes, returning the 171 // requirements found. 172 // 173 // This is intended to maximize the chance that we'll be able to read the 174 // requirements (syntax errors notwithstanding) even if the config file contains 175 // constructs that might've been added in future Terraform versions 176 // 177 // This is a "best effort" sort of method which will return constraints it is 178 // able to find, but may return no constraints at all if the given body is 179 // so invalid that it cannot be decoded at all. 180 func sniffCoreVersionRequirements(body hcl.Body) ([]VersionConstraint, hcl.Diagnostics) { 181 rootContent, _, diags := body.PartialContent(configFileTerraformBlockSniffRootSchema) 182 183 var constraints []VersionConstraint 184 185 for _, block := range rootContent.Blocks { 186 content, _, blockDiags := block.Body.PartialContent(configFileVersionSniffBlockSchema) 187 diags = append(diags, blockDiags...) 188 189 attr, exists := content.Attributes["required_version"] 190 if !exists { 191 continue 192 } 193 194 constraint, constraintDiags := decodeVersionConstraint(attr) 195 diags = append(diags, constraintDiags...) 196 if !constraintDiags.HasErrors() { 197 constraints = append(constraints, constraint) 198 } 199 } 200 201 return constraints, diags 202 } 203 204 // configFileSchema is the schema for the top-level of a config file. We use 205 // the low-level HCL API for this level so we can easily deal with each 206 // block type separately with its own decoding logic. 207 var configFileSchema = &hcl.BodySchema{ 208 Blocks: []hcl.BlockHeaderSchema{ 209 { 210 Type: "terraform", 211 }, 212 { 213 // This one is not really valid, but we include it here so we 214 // can create a specialized error message hinting the user to 215 // nest it inside a "terraform" block. 216 Type: "required_providers", 217 }, 218 { 219 Type: "provider", 220 LabelNames: []string{"name"}, 221 }, 222 { 223 Type: "variable", 224 LabelNames: []string{"name"}, 225 }, 226 { 227 Type: "locals", 228 }, 229 { 230 Type: "output", 231 LabelNames: []string{"name"}, 232 }, 233 { 234 Type: "module", 235 LabelNames: []string{"name"}, 236 }, 237 { 238 Type: "resource", 239 LabelNames: []string{"type", "name"}, 240 }, 241 { 242 Type: "data", 243 LabelNames: []string{"type", "name"}, 244 }, 245 { 246 Type: "moved", 247 }, 248 }, 249 } 250 251 // terraformBlockSchema is the schema for a top-level "terraform" block in 252 // a configuration file. 253 var terraformBlockSchema = &hcl.BodySchema{ 254 Attributes: []hcl.AttributeSchema{ 255 {Name: "required_version"}, 256 {Name: "experiments"}, 257 {Name: "language"}, 258 }, 259 Blocks: []hcl.BlockHeaderSchema{ 260 { 261 Type: "backend", 262 LabelNames: []string{"type"}, 263 }, 264 { 265 Type: "required_providers", 266 }, 267 { 268 Type: "provider_meta", 269 LabelNames: []string{"provider"}, 270 }, 271 }, 272 } 273 274 // configFileTerraformBlockSniffRootSchema is a schema for 275 // sniffCoreVersionRequirements and sniffActiveExperiments. 276 var configFileTerraformBlockSniffRootSchema = &hcl.BodySchema{ 277 Blocks: []hcl.BlockHeaderSchema{ 278 { 279 Type: "terraform", 280 }, 281 }, 282 } 283 284 // configFileVersionSniffBlockSchema is a schema for sniffCoreVersionRequirements 285 var configFileVersionSniffBlockSchema = &hcl.BodySchema{ 286 Attributes: []hcl.AttributeSchema{ 287 { 288 Name: "required_version", 289 }, 290 }, 291 } 292 293 // configFileExperimentsSniffBlockSchema is a schema for sniffActiveExperiments, 294 // to decode a single attribute from inside a "terraform" block. 295 var configFileExperimentsSniffBlockSchema = &hcl.BodySchema{ 296 Attributes: []hcl.AttributeSchema{ 297 {Name: "experiments"}, 298 {Name: "language"}, 299 }, 300 }