github.com/diggerhq/digger/libs@v0.0.0-20240604170430-9d61cdf01cc5/digger_config/terragrunt/atlantis/pares_hcl.go (about)

     1  package atlantis
     2  
     3  import (
     4  	"github.com/gruntwork-io/go-commons/errors"
     5  	"github.com/gruntwork-io/terragrunt/config"
     6  	"github.com/gruntwork-io/terragrunt/options"
     7  	"github.com/gruntwork-io/terragrunt/util"
     8  	"github.com/hashicorp/hcl/v2"
     9  	"github.com/hashicorp/hcl/v2/gohcl"
    10  	"github.com/hashicorp/hcl/v2/hclparse"
    11  	"github.com/hashicorp/hcl/v2/hclwrite"
    12  	"path/filepath"
    13  )
    14  
    15  const bareIncludeKey = ""
    16  
    17  type parsedHcl struct {
    18  	Terraform *config.TerraformConfig `hcl:"terraform,block"`
    19  	Includes  []config.IncludeConfig  `hcl:"include,block"`
    20  }
    21  
    22  // terragruntIncludeMultiple is a struct that can be used to only decode the include block with labels.
    23  type terragruntIncludeMultiple struct {
    24  	Include []config.IncludeConfig `hcl:"include,block"`
    25  	Remain  hcl.Body               `hcl:",remain"`
    26  }
    27  
    28  // updateBareIncludeBlock searches the parsed terragrunt contents for a bare include block (include without a label),
    29  // and convert it to one with empty string as the label. This is necessary because the hcl parser is strictly enforces
    30  // label counts when parsing out labels with a go struct.
    31  //
    32  // Returns the updated contents, a boolean indicated whether anything changed, and an error (if any).
    33  func updateBareIncludeBlock(file *hcl.File, filename string) ([]byte, bool, error) {
    34  	hclFile, err := hclwrite.ParseConfig(file.Bytes, filename, hcl.InitialPos)
    35  	if err != nil {
    36  		return nil, false, errors.WithStackTrace(err)
    37  	}
    38  
    39  	codeWasUpdated := false
    40  	for _, block := range hclFile.Body().Blocks() {
    41  		if block.Type() == "include" && len(block.Labels()) == 0 {
    42  			if codeWasUpdated {
    43  				return nil, false, errors.WithStackTrace(config.MultipleBareIncludeBlocksErr{})
    44  			}
    45  			block.SetLabels([]string{bareIncludeKey})
    46  			codeWasUpdated = true
    47  		}
    48  	}
    49  	return hclFile.Bytes(), codeWasUpdated, nil
    50  }
    51  
    52  // decodeHcl uses the HCL2 parser to decode the parsed HCL into the struct specified by out.
    53  //
    54  // Note that we take a two pass approach to support parsing include blocks without a label. Ideally we can parse include
    55  // blocks with and without labels in a single pass, but the HCL parser is fairly restrictive when it comes to parsing
    56  // blocks with labels, requiring the exact number of expected labels in the parsing step.  To handle this restriction,
    57  // we first see if there are any include blocks without any labels, and if there is, we modify it in the file object to
    58  // inject the label as "".
    59  func decodeHcl(
    60  	file *hcl.File,
    61  	filename string,
    62  	out interface{},
    63  	terragruntOptions *options.TerragruntOptions,
    64  	extensions config.EvalContextExtensions,
    65  ) (err error) {
    66  	// The HCL2 parser and especially cty conversions will panic in many types of errors, so we have to recover from
    67  	// those panics here and convert them to normal errors
    68  	defer func() {
    69  		if recovered := recover(); recovered != nil {
    70  			err = errors.WithStackTrace(config.PanicWhileParsingConfig{RecoveredValue: recovered, ConfigFile: filename})
    71  		}
    72  	}()
    73  
    74  	// Check if we need to update the file to label any bare include blocks.
    75  	// Excluding json because of https://github.com/transcend-io/terragrunt-atlantis-config/issues/244.
    76  	if filepath.Ext(filename) != ".json" {
    77  		updatedBytes, isUpdated, err := updateBareIncludeBlock(file, filename)
    78  		if err != nil {
    79  			return err
    80  		}
    81  		if isUpdated {
    82  			// Code was updated, so we need to reparse the new updated contents. This is necessarily because the blocks
    83  			// returned by hclparse does not support editing, and so we have to go through hclwrite, which leads to a
    84  			// different AST representation.
    85  			file, err = parseHcl(hclparse.NewParser(), string(updatedBytes), filename)
    86  			if err != nil {
    87  				return err
    88  			}
    89  		}
    90  	}
    91  	evalContext, err := extensions.CreateTerragruntEvalContext(filename, terragruntOptions)
    92  	if err != nil {
    93  		return err
    94  	}
    95  
    96  	decodeDiagnostics := gohcl.DecodeBody(file.Body, evalContext, out)
    97  	if decodeDiagnostics != nil && decodeDiagnostics.HasErrors() {
    98  		return decodeDiagnostics
    99  	}
   100  
   101  	return nil
   102  }
   103  
   104  // This decodes only the `include` blocks of a terragrunt digger_config, so its value can be used while decoding the rest of
   105  // the digger_config.
   106  // For consistency, `include` in the call to `decodeHcl` is always assumed to be nil. Either it really is nil (parsing
   107  // the child digger_config), or it shouldn't be used anyway (the parent digger_config shouldn't have an include block).
   108  func decodeAsTerragruntInclude(
   109  	file *hcl.File,
   110  	filename string,
   111  	terragruntOptions *options.TerragruntOptions,
   112  	extensions config.EvalContextExtensions,
   113  ) ([]config.IncludeConfig, error) {
   114  	tgInc := terragruntIncludeMultiple{}
   115  	if err := decodeHcl(file, filename, &tgInc, terragruntOptions, extensions); err != nil {
   116  		return nil, err
   117  	}
   118  	return tgInc.Include, nil
   119  }
   120  
   121  // Not all modules need an include statement, as they could define everything in one file without a parent
   122  // The key signifiers of a parent are:
   123  //   - no include statement
   124  //   - no terraform source defined
   125  //
   126  // If both of those are true, it is likely a parent module
   127  func parseModule(path string, terragruntOptions *options.TerragruntOptions) (isParent bool, includes []config.IncludeConfig, err error) {
   128  	configString, err := util.ReadFileAsString(path)
   129  	if err != nil {
   130  		return false, nil, err
   131  	}
   132  
   133  	parser := hclparse.NewParser()
   134  	file, err := parseHcl(parser, configString, path)
   135  	if err != nil {
   136  		return false, nil, err
   137  	}
   138  
   139  	// Decode just the `include` and `import` blocks, and verify that it's allowed here
   140  	extensions := config.EvalContextExtensions{}
   141  	terragruntIncludeList, err := decodeAsTerragruntInclude(file, path, terragruntOptions, extensions)
   142  	if err != nil {
   143  		return false, nil, err
   144  	}
   145  
   146  	// If the file has any `include` blocks it is not a parent
   147  	if len(terragruntIncludeList) > 0 {
   148  		return false, terragruntIncludeList, nil
   149  	}
   150  
   151  	// We don't need to check the errors/diagnostics coming from `decodeHcl`, as when errors come up,
   152  	// it will leave the partially parsed result in the output object.
   153  	var parsed parsedHcl
   154  	decodeHcl(file, path, &parsed, terragruntOptions, extensions)
   155  
   156  	// If the file does not define a terraform source block, it is likely a parent (though not guaranteed)
   157  	if parsed.Terraform == nil || parsed.Terraform.Source == nil {
   158  		return true, nil, nil
   159  	}
   160  
   161  	return false, nil, nil
   162  }