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 }