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

     1  package atlantis
     2  
     3  // Terragrunt doesn't give us an easy way to access all of the Locals from a module
     4  // in an easy to digest way. This file is mostly just follows along how Terragrunt
     5  // parses the `locals` blocks and evaluates their contents.
     6  
     7  import (
     8  	"github.com/gruntwork-io/go-commons/errors"
     9  	"github.com/gruntwork-io/terragrunt/config"
    10  	"github.com/gruntwork-io/terragrunt/options"
    11  	"github.com/gruntwork-io/terragrunt/util"
    12  	"github.com/hashicorp/hcl/v2"
    13  	"github.com/hashicorp/hcl/v2/hclparse"
    14  	"github.com/zclconf/go-cty/cty"
    15  
    16  	"path/filepath"
    17  )
    18  
    19  // ResolvedLocals are the parsed result of local values this module cares about
    20  type ResolvedLocals struct {
    21  	// The Atlantis workflow to use for some project
    22  	AtlantisWorkflow string
    23  
    24  	// Apply requirements to override the global `--apply-requirements` flag
    25  	ApplyRequirements []string
    26  
    27  	// Extra dependencies that can be hardcoded in digger_config
    28  	ExtraAtlantisDependencies []string
    29  
    30  	// If set, a single module will have autoplan turned to this setting
    31  	AutoPlan *bool
    32  
    33  	// If set to true, the module will not be included in the output
    34  	Skip *bool
    35  
    36  	// Terraform version to use just for this project
    37  	TerraformVersion string
    38  
    39  	// If set to true, create Atlantis project
    40  	markedProject *bool
    41  }
    42  
    43  // parseHcl uses the HCL2 parser to parse the given string into an HCL file body.
    44  func parseHcl(parser *hclparse.Parser, hcl string, filename string) (file *hcl.File, err error) {
    45  	// The HCL2 parser and especially cty conversions will panic in many types of errors, so we have to recover from
    46  	// those panics here and convert them to normal errors
    47  	defer func() {
    48  		if recovered := recover(); recovered != nil {
    49  			err = errors.WithStackTrace(config.PanicWhileParsingConfig{RecoveredValue: recovered, ConfigFile: filename})
    50  		}
    51  	}()
    52  
    53  	if filepath.Ext(filename) == ".json" {
    54  		file, parseDiagnostics := parser.ParseJSON([]byte(hcl), filename)
    55  		if parseDiagnostics != nil && parseDiagnostics.HasErrors() {
    56  			return nil, parseDiagnostics
    57  		}
    58  
    59  		return file, nil
    60  	}
    61  
    62  	file, parseDiagnostics := parser.ParseHCL([]byte(hcl), filename)
    63  	if parseDiagnostics != nil && parseDiagnostics.HasErrors() {
    64  		return nil, parseDiagnostics
    65  	}
    66  
    67  	return file, nil
    68  }
    69  
    70  // Merges in values from a child into a parent set of `local` values
    71  func mergeResolvedLocals(parent ResolvedLocals, child ResolvedLocals) ResolvedLocals {
    72  	if child.AtlantisWorkflow != "" {
    73  		parent.AtlantisWorkflow = child.AtlantisWorkflow
    74  	}
    75  
    76  	if child.TerraformVersion != "" {
    77  		parent.TerraformVersion = child.TerraformVersion
    78  	}
    79  
    80  	if child.AutoPlan != nil {
    81  		parent.AutoPlan = child.AutoPlan
    82  	}
    83  
    84  	if child.Skip != nil {
    85  		parent.Skip = child.Skip
    86  	}
    87  
    88  	if child.markedProject != nil {
    89  		parent.markedProject = child.markedProject
    90  	}
    91  
    92  	if child.ApplyRequirements != nil || len(child.ApplyRequirements) > 0 {
    93  		parent.ApplyRequirements = child.ApplyRequirements
    94  	}
    95  
    96  	parent.ExtraAtlantisDependencies = append(parent.ExtraAtlantisDependencies, child.ExtraAtlantisDependencies...)
    97  
    98  	return parent
    99  }
   100  
   101  // Parses a given file, returning a map of all it's `local` values
   102  func parseLocals(path string, terragruntOptions *options.TerragruntOptions, includeFromChild *config.IncludeConfig) (ResolvedLocals, error) {
   103  	configString, err := util.ReadFileAsString(path)
   104  	if err != nil {
   105  		return ResolvedLocals{}, err
   106  	}
   107  
   108  	// Parse the HCL string into an AST body
   109  	parser := hclparse.NewParser()
   110  	file, err := parseHcl(parser, configString, path)
   111  	if err != nil {
   112  		return ResolvedLocals{}, err
   113  	}
   114  
   115  	// Decode just the Base blocks. See the function docs for DecodeBaseBlocks for more info on what base blocks are.
   116  	extensions, err := config.DecodeBaseBlocks(terragruntOptions, parser, file, path, includeFromChild, nil)
   117  	if err != nil {
   118  		return ResolvedLocals{}, err
   119  	}
   120  	localsAsCty := extensions.Locals
   121  	trackInclude := extensions.TrackInclude
   122  
   123  	// Recurse on the parent to merge in the locals from that file
   124  	mergedParentLocals := ResolvedLocals{}
   125  	if trackInclude != nil && includeFromChild == nil {
   126  		for _, includeConfig := range trackInclude.CurrentList {
   127  			parentLocals, _ := parseLocals(includeConfig.Path, terragruntOptions, &includeConfig)
   128  			mergedParentLocals = mergeResolvedLocals(mergedParentLocals, parentLocals)
   129  		}
   130  	}
   131  	childLocals := resolveLocals(*localsAsCty)
   132  
   133  	return mergeResolvedLocals(mergedParentLocals, childLocals), nil
   134  }
   135  
   136  func resolveLocals(localsAsCty cty.Value) ResolvedLocals {
   137  	resolved := ResolvedLocals{}
   138  
   139  	// Return an empty set of locals if no `locals` block was present
   140  	if localsAsCty == cty.NilVal {
   141  		return resolved
   142  	}
   143  	rawLocals := localsAsCty.AsValueMap()
   144  
   145  	workflowValue, ok := rawLocals["atlantis_workflow"]
   146  	if ok {
   147  		resolved.AtlantisWorkflow = workflowValue.AsString()
   148  	}
   149  
   150  	versionValue, ok := rawLocals["atlantis_terraform_version"]
   151  	if ok {
   152  		resolved.TerraformVersion = versionValue.AsString()
   153  	}
   154  
   155  	autoPlanValue, ok := rawLocals["atlantis_autoplan"]
   156  	if ok {
   157  		hasValue := autoPlanValue.True()
   158  		resolved.AutoPlan = &hasValue
   159  	}
   160  
   161  	skipValue, ok := rawLocals["atlantis_skip"]
   162  	if ok {
   163  		hasValue := skipValue.True()
   164  		resolved.Skip = &hasValue
   165  	}
   166  
   167  	applyReqs, ok := rawLocals["atlantis_apply_requirements"]
   168  	if ok {
   169  		resolved.ApplyRequirements = []string{}
   170  		it := applyReqs.ElementIterator()
   171  		for it.Next() {
   172  			_, val := it.Element()
   173  			resolved.ApplyRequirements = append(resolved.ApplyRequirements, val.AsString())
   174  		}
   175  	}
   176  
   177  	markedProject, ok := rawLocals["atlantis_project"]
   178  	if ok {
   179  		hasValue := markedProject.True()
   180  		resolved.markedProject = &hasValue
   181  	}
   182  
   183  	extraDependenciesAsCty, ok := rawLocals["extra_atlantis_dependencies"]
   184  	if ok {
   185  		it := extraDependenciesAsCty.ElementIterator()
   186  		for it.Next() {
   187  			_, val := it.Element()
   188  			resolved.ExtraAtlantisDependencies = append(
   189  				resolved.ExtraAtlantisDependencies,
   190  				filepath.ToSlash(val.AsString()),
   191  			)
   192  		}
   193  	}
   194  
   195  	return resolved
   196  }