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  }