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