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  }