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  }