kubeform.dev/terraform-backend-sdk@v0.0.0-20220310143633-45f07fe731c5/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  		case "moved":
   152  			cfg, cfgDiags := decodeMovedBlock(block)
   153  			diags = append(diags, cfgDiags...)
   154  			if cfg != nil {
   155  				file.Moved = append(file.Moved, cfg)
   156  			}
   157  
   158  		default:
   159  			// Should never happen because the above cases should be exhaustive
   160  			// for all block type names in our schema.
   161  			continue
   162  
   163  		}
   164  	}
   165  
   166  	return file, diags
   167  }
   168  
   169  // sniffCoreVersionRequirements does minimal parsing of the given body for
   170  // "terraform" blocks with "required_version" attributes, returning the
   171  // requirements found.
   172  //
   173  // This is intended to maximize the chance that we'll be able to read the
   174  // requirements (syntax errors notwithstanding) even if the config file contains
   175  // constructs that might've been added in future Terraform versions
   176  //
   177  // This is a "best effort" sort of method which will return constraints it is
   178  // able to find, but may return no constraints at all if the given body is
   179  // so invalid that it cannot be decoded at all.
   180  func sniffCoreVersionRequirements(body hcl.Body) ([]VersionConstraint, hcl.Diagnostics) {
   181  	rootContent, _, diags := body.PartialContent(configFileTerraformBlockSniffRootSchema)
   182  
   183  	var constraints []VersionConstraint
   184  
   185  	for _, block := range rootContent.Blocks {
   186  		content, _, blockDiags := block.Body.PartialContent(configFileVersionSniffBlockSchema)
   187  		diags = append(diags, blockDiags...)
   188  
   189  		attr, exists := content.Attributes["required_version"]
   190  		if !exists {
   191  			continue
   192  		}
   193  
   194  		constraint, constraintDiags := decodeVersionConstraint(attr)
   195  		diags = append(diags, constraintDiags...)
   196  		if !constraintDiags.HasErrors() {
   197  			constraints = append(constraints, constraint)
   198  		}
   199  	}
   200  
   201  	return constraints, diags
   202  }
   203  
   204  // configFileSchema is the schema for the top-level of a config file. We use
   205  // the low-level HCL API for this level so we can easily deal with each
   206  // block type separately with its own decoding logic.
   207  var configFileSchema = &hcl.BodySchema{
   208  	Blocks: []hcl.BlockHeaderSchema{
   209  		{
   210  			Type: "terraform",
   211  		},
   212  		{
   213  			// This one is not really valid, but we include it here so we
   214  			// can create a specialized error message hinting the user to
   215  			// nest it inside a "terraform" block.
   216  			Type: "required_providers",
   217  		},
   218  		{
   219  			Type:       "provider",
   220  			LabelNames: []string{"name"},
   221  		},
   222  		{
   223  			Type:       "variable",
   224  			LabelNames: []string{"name"},
   225  		},
   226  		{
   227  			Type: "locals",
   228  		},
   229  		{
   230  			Type:       "output",
   231  			LabelNames: []string{"name"},
   232  		},
   233  		{
   234  			Type:       "module",
   235  			LabelNames: []string{"name"},
   236  		},
   237  		{
   238  			Type:       "resource",
   239  			LabelNames: []string{"type", "name"},
   240  		},
   241  		{
   242  			Type:       "data",
   243  			LabelNames: []string{"type", "name"},
   244  		},
   245  		{
   246  			Type: "moved",
   247  		},
   248  	},
   249  }
   250  
   251  // terraformBlockSchema is the schema for a top-level "terraform" block in
   252  // a configuration file.
   253  var terraformBlockSchema = &hcl.BodySchema{
   254  	Attributes: []hcl.AttributeSchema{
   255  		{Name: "required_version"},
   256  		{Name: "experiments"},
   257  		{Name: "language"},
   258  	},
   259  	Blocks: []hcl.BlockHeaderSchema{
   260  		{
   261  			Type:       "backend",
   262  			LabelNames: []string{"type"},
   263  		},
   264  		{
   265  			Type: "required_providers",
   266  		},
   267  		{
   268  			Type:       "provider_meta",
   269  			LabelNames: []string{"provider"},
   270  		},
   271  	},
   272  }
   273  
   274  // configFileTerraformBlockSniffRootSchema is a schema for
   275  // sniffCoreVersionRequirements and sniffActiveExperiments.
   276  var configFileTerraformBlockSniffRootSchema = &hcl.BodySchema{
   277  	Blocks: []hcl.BlockHeaderSchema{
   278  		{
   279  			Type: "terraform",
   280  		},
   281  	},
   282  }
   283  
   284  // configFileVersionSniffBlockSchema is a schema for sniffCoreVersionRequirements
   285  var configFileVersionSniffBlockSchema = &hcl.BodySchema{
   286  	Attributes: []hcl.AttributeSchema{
   287  		{
   288  			Name: "required_version",
   289  		},
   290  	},
   291  }
   292  
   293  // configFileExperimentsSniffBlockSchema is a schema for sniffActiveExperiments,
   294  // to decode a single attribute from inside a "terraform" block.
   295  var configFileExperimentsSniffBlockSchema = &hcl.BodySchema{
   296  	Attributes: []hcl.AttributeSchema{
   297  		{Name: "experiments"},
   298  		{Name: "language"},
   299  	},
   300  }