github.com/rvichery/terraform@v0.11.10/configs/parser_config.go (about) 1 package configs 2 3 import ( 4 "github.com/hashicorp/hcl2/hcl" 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 content, contentDiags := body.Content(configFileSchema) 46 diags = append(diags, contentDiags...) 47 48 for _, block := range content.Blocks { 49 switch block.Type { 50 51 case "terraform": 52 content, contentDiags := block.Body.Content(terraformBlockSchema) 53 diags = append(diags, contentDiags...) 54 55 // We ignore the "terraform_version" attribute here because 56 // sniffCoreVersionRequirements already dealt with that above. 57 58 for _, innerBlock := range content.Blocks { 59 switch innerBlock.Type { 60 61 case "backend": 62 backendCfg, cfgDiags := decodeBackendBlock(innerBlock) 63 diags = append(diags, cfgDiags...) 64 if backendCfg != nil { 65 file.Backends = append(file.Backends, backendCfg) 66 } 67 68 case "required_providers": 69 reqs, reqsDiags := decodeRequiredProvidersBlock(innerBlock) 70 diags = append(diags, reqsDiags...) 71 file.ProviderRequirements = append(file.ProviderRequirements, reqs...) 72 73 default: 74 // Should never happen because the above cases should be exhaustive 75 // for all block type names in our schema. 76 continue 77 78 } 79 } 80 81 case "provider": 82 cfg, cfgDiags := decodeProviderBlock(block) 83 diags = append(diags, cfgDiags...) 84 if cfg != nil { 85 file.ProviderConfigs = append(file.ProviderConfigs, cfg) 86 } 87 88 case "variable": 89 cfg, cfgDiags := decodeVariableBlock(block, override) 90 diags = append(diags, cfgDiags...) 91 if cfg != nil { 92 file.Variables = append(file.Variables, cfg) 93 } 94 95 case "locals": 96 defs, defsDiags := decodeLocalsBlock(block) 97 diags = append(diags, defsDiags...) 98 file.Locals = append(file.Locals, defs...) 99 100 case "output": 101 cfg, cfgDiags := decodeOutputBlock(block, override) 102 diags = append(diags, cfgDiags...) 103 if cfg != nil { 104 file.Outputs = append(file.Outputs, cfg) 105 } 106 107 case "module": 108 cfg, cfgDiags := decodeModuleBlock(block, override) 109 diags = append(diags, cfgDiags...) 110 if cfg != nil { 111 file.ModuleCalls = append(file.ModuleCalls, cfg) 112 } 113 114 case "resource": 115 cfg, cfgDiags := decodeResourceBlock(block) 116 diags = append(diags, cfgDiags...) 117 if cfg != nil { 118 file.ManagedResources = append(file.ManagedResources, cfg) 119 } 120 121 case "data": 122 cfg, cfgDiags := decodeDataBlock(block) 123 diags = append(diags, cfgDiags...) 124 if cfg != nil { 125 file.DataResources = append(file.DataResources, cfg) 126 } 127 128 default: 129 // Should never happen because the above cases should be exhaustive 130 // for all block type names in our schema. 131 continue 132 133 } 134 } 135 136 return file, diags 137 } 138 139 // sniffCoreVersionRequirements does minimal parsing of the given body for 140 // "terraform" blocks with "required_version" attributes, returning the 141 // requirements found. 142 // 143 // This is intended to maximize the chance that we'll be able to read the 144 // requirements (syntax errors notwithstanding) even if the config file contains 145 // constructs that might've been added in future Terraform versions 146 // 147 // This is a "best effort" sort of method which will return constraints it is 148 // able to find, but may return no constraints at all if the given body is 149 // so invalid that it cannot be decoded at all. 150 func sniffCoreVersionRequirements(body hcl.Body) ([]VersionConstraint, hcl.Diagnostics) { 151 rootContent, _, diags := body.PartialContent(configFileVersionSniffRootSchema) 152 153 var constraints []VersionConstraint 154 155 for _, block := range rootContent.Blocks { 156 content, _, blockDiags := block.Body.PartialContent(configFileVersionSniffBlockSchema) 157 diags = append(diags, blockDiags...) 158 159 attr, exists := content.Attributes["required_version"] 160 if !exists { 161 continue 162 } 163 164 constraint, constraintDiags := decodeVersionConstraint(attr) 165 diags = append(diags, constraintDiags...) 166 if !constraintDiags.HasErrors() { 167 constraints = append(constraints, constraint) 168 } 169 } 170 171 return constraints, diags 172 } 173 174 // configFileSchema is the schema for the top-level of a config file. We use 175 // the low-level HCL API for this level so we can easily deal with each 176 // block type separately with its own decoding logic. 177 var configFileSchema = &hcl.BodySchema{ 178 Blocks: []hcl.BlockHeaderSchema{ 179 { 180 Type: "terraform", 181 }, 182 { 183 Type: "provider", 184 LabelNames: []string{"name"}, 185 }, 186 { 187 Type: "variable", 188 LabelNames: []string{"name"}, 189 }, 190 { 191 Type: "locals", 192 }, 193 { 194 Type: "output", 195 LabelNames: []string{"name"}, 196 }, 197 { 198 Type: "module", 199 LabelNames: []string{"name"}, 200 }, 201 { 202 Type: "resource", 203 LabelNames: []string{"type", "name"}, 204 }, 205 { 206 Type: "data", 207 LabelNames: []string{"type", "name"}, 208 }, 209 }, 210 } 211 212 // terraformBlockSchema is the schema for a top-level "terraform" block in 213 // a configuration file. 214 var terraformBlockSchema = &hcl.BodySchema{ 215 Attributes: []hcl.AttributeSchema{ 216 { 217 Name: "required_version", 218 }, 219 }, 220 Blocks: []hcl.BlockHeaderSchema{ 221 { 222 Type: "backend", 223 LabelNames: []string{"type"}, 224 }, 225 { 226 Type: "required_providers", 227 }, 228 }, 229 } 230 231 // configFileVersionSniffRootSchema is a schema for sniffCoreVersionRequirements 232 var configFileVersionSniffRootSchema = &hcl.BodySchema{ 233 Blocks: []hcl.BlockHeaderSchema{ 234 { 235 Type: "terraform", 236 }, 237 }, 238 } 239 240 // configFileVersionSniffBlockSchema is a schema for sniffCoreVersionRequirements 241 var configFileVersionSniffBlockSchema = &hcl.BodySchema{ 242 Attributes: []hcl.AttributeSchema{ 243 { 244 Name: "required_version", 245 }, 246 }, 247 }