github.com/iaas-resource-provision/iaas-rpc@v1.0.7-0.20211021023331-ed21f798c408/internal/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 default: 152 // Should never happen because the above cases should be exhaustive 153 // for all block type names in our schema. 154 continue 155 156 } 157 } 158 159 return file, diags 160 } 161 162 // sniffCoreVersionRequirements does minimal parsing of the given body for 163 // "terraform" blocks with "required_version" attributes, returning the 164 // requirements found. 165 // 166 // This is intended to maximize the chance that we'll be able to read the 167 // requirements (syntax errors notwithstanding) even if the config file contains 168 // constructs that might've been added in future Terraform versions 169 // 170 // This is a "best effort" sort of method which will return constraints it is 171 // able to find, but may return no constraints at all if the given body is 172 // so invalid that it cannot be decoded at all. 173 func sniffCoreVersionRequirements(body hcl.Body) ([]VersionConstraint, hcl.Diagnostics) { 174 rootContent, _, diags := body.PartialContent(configFileTerraformBlockSniffRootSchema) 175 176 var constraints []VersionConstraint 177 178 for _, block := range rootContent.Blocks { 179 content, _, blockDiags := block.Body.PartialContent(configFileVersionSniffBlockSchema) 180 diags = append(diags, blockDiags...) 181 182 attr, exists := content.Attributes["required_version"] 183 if !exists { 184 continue 185 } 186 187 constraint, constraintDiags := decodeVersionConstraint(attr) 188 diags = append(diags, constraintDiags...) 189 if !constraintDiags.HasErrors() { 190 constraints = append(constraints, constraint) 191 } 192 } 193 194 return constraints, diags 195 } 196 197 // configFileSchema is the schema for the top-level of a config file. We use 198 // the low-level HCL API for this level so we can easily deal with each 199 // block type separately with its own decoding logic. 200 var configFileSchema = &hcl.BodySchema{ 201 Blocks: []hcl.BlockHeaderSchema{ 202 { 203 Type: "terraform", 204 }, 205 { 206 // This one is not really valid, but we include it here so we 207 // can create a specialized error message hinting the user to 208 // nest it inside a "terraform" block. 209 Type: "required_providers", 210 }, 211 { 212 Type: "provider", 213 LabelNames: []string{"name"}, 214 }, 215 { 216 Type: "variable", 217 LabelNames: []string{"name"}, 218 }, 219 { 220 Type: "locals", 221 }, 222 { 223 Type: "output", 224 LabelNames: []string{"name"}, 225 }, 226 { 227 Type: "module", 228 LabelNames: []string{"name"}, 229 }, 230 { 231 Type: "resource", 232 LabelNames: []string{"type", "name"}, 233 }, 234 { 235 Type: "data", 236 LabelNames: []string{"type", "name"}, 237 }, 238 }, 239 } 240 241 // terraformBlockSchema is the schema for a top-level "terraform" block in 242 // a configuration file. 243 var terraformBlockSchema = &hcl.BodySchema{ 244 Attributes: []hcl.AttributeSchema{ 245 {Name: "required_version"}, 246 {Name: "experiments"}, 247 {Name: "language"}, 248 }, 249 Blocks: []hcl.BlockHeaderSchema{ 250 { 251 Type: "backend", 252 LabelNames: []string{"type"}, 253 }, 254 { 255 Type: "required_providers", 256 }, 257 { 258 Type: "provider_meta", 259 LabelNames: []string{"provider"}, 260 }, 261 }, 262 } 263 264 // configFileTerraformBlockSniffRootSchema is a schema for 265 // sniffCoreVersionRequirements and sniffActiveExperiments. 266 var configFileTerraformBlockSniffRootSchema = &hcl.BodySchema{ 267 Blocks: []hcl.BlockHeaderSchema{ 268 { 269 Type: "terraform", 270 }, 271 }, 272 } 273 274 // configFileVersionSniffBlockSchema is a schema for sniffCoreVersionRequirements 275 var configFileVersionSniffBlockSchema = &hcl.BodySchema{ 276 Attributes: []hcl.AttributeSchema{ 277 { 278 Name: "required_version", 279 }, 280 }, 281 } 282 283 // configFileExperimentsSniffBlockSchema is a schema for sniffActiveExperiments, 284 // to decode a single attribute from inside a "terraform" block. 285 var configFileExperimentsSniffBlockSchema = &hcl.BodySchema{ 286 Attributes: []hcl.AttributeSchema{ 287 {Name: "experiments"}, 288 {Name: "language"}, 289 }, 290 }