github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/steampipeconfig/parse/mod_parse_context.go (about)

     1  package parse
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  
     7  	"github.com/hashicorp/hcl/v2"
     8  	"github.com/hashicorp/hcl/v2/hclsyntax"
     9  	filehelpers "github.com/turbot/go-kit/files"
    10  	"github.com/turbot/go-kit/helpers"
    11  	"github.com/turbot/pipe-fittings/hclhelpers"
    12  	"github.com/turbot/steampipe/pkg/steampipeconfig/inputvars"
    13  	"github.com/turbot/steampipe/pkg/steampipeconfig/modconfig"
    14  	"github.com/turbot/steampipe/pkg/steampipeconfig/versionmap"
    15  	"github.com/turbot/steampipe/pkg/utils"
    16  	"github.com/zclconf/go-cty/cty"
    17  )
    18  
    19  const rootDependencyNode = "rootDependencyNode"
    20  
    21  type ParseModFlag uint32
    22  
    23  const (
    24  	CreateDefaultMod ParseModFlag = 1 << iota
    25  	CreatePseudoResources
    26  )
    27  
    28  /*
    29  	ReferenceTypeValueMap is the raw data used to build the evaluation context
    30  
    31  When resolving hcl references like :
    32  - query.q1
    33  - var.v1
    34  - mod1.query.my_query.sql
    35  
    36  ReferenceTypeValueMap is keyed  by resource type, then by resource name
    37  */
    38  type ReferenceTypeValueMap map[string]map[string]cty.Value
    39  
    40  type ModParseContext struct {
    41  	ParseContext
    42  	// the mod which is currently being parsed
    43  	CurrentMod *modconfig.Mod
    44  	// the workspace lock data
    45  	WorkspaceLock *versionmap.WorkspaceLock
    46  
    47  	Flags       ParseModFlag
    48  	ListOptions *filehelpers.ListOptions
    49  
    50  	// Variables are populated in an initial parse pass top we store them on the run context
    51  	// so we can set them on the mod when we do the main parse
    52  
    53  	// Variables is a tree of maps of the variables in the current mod and child dependency mods
    54  	Variables *modconfig.ModVariableMap
    55  
    56  	ParentParseCtx *ModParseContext
    57  
    58  	// stack of parent resources for the currently parsed block
    59  	// (unqualified name)
    60  	parents []string
    61  
    62  	// map of resource children, keyed by parent unqualified name
    63  	blockChildMap map[string][]string
    64  
    65  	// map of top  level blocks, for easy checking
    66  	topLevelBlocks map[*hcl.Block]struct{}
    67  	// map of block names, keyed by a hash of the blopck
    68  	blockNameMap map[string]string
    69  	// map of ReferenceTypeValueMaps keyed by mod name
    70  	// NOTE: all values from root mod are keyed with "local"
    71  	referenceValues map[string]ReferenceTypeValueMap
    72  
    73  	// a map of just the top level dependencies of the CurrentMod, keyed my full mod DependencyName (with no version)
    74  	topLevelDependencyMods modconfig.ModMap
    75  	// if we are loading dependency mod, this contains the details
    76  	DependencyConfig *ModDependencyConfig
    77  }
    78  
    79  func NewModParseContext(workspaceLock *versionmap.WorkspaceLock, rootEvalPath string, flags ParseModFlag, listOptions *filehelpers.ListOptions) *ModParseContext {
    80  	parseContext := NewParseContext(rootEvalPath)
    81  	c := &ModParseContext{
    82  		ParseContext:  parseContext,
    83  		Flags:         flags,
    84  		WorkspaceLock: workspaceLock,
    85  		ListOptions:   listOptions,
    86  
    87  		topLevelDependencyMods: make(modconfig.ModMap),
    88  		blockChildMap:          make(map[string][]string),
    89  		blockNameMap:           make(map[string]string),
    90  		// initialise reference maps - even though we later overwrite them
    91  		referenceValues: map[string]ReferenceTypeValueMap{
    92  			"local": make(ReferenceTypeValueMap),
    93  		},
    94  	}
    95  	// add root node - this will depend on all other nodes
    96  	c.dependencyGraph = c.newDependencyGraph()
    97  	c.buildEvalContext()
    98  
    99  	return c
   100  }
   101  
   102  func NewChildModParseContext(parent *ModParseContext, modVersion *versionmap.ResolvedVersionConstraint, rootEvalPath string) *ModParseContext {
   103  	// create a child run context
   104  	child := NewModParseContext(
   105  		parent.WorkspaceLock,
   106  		rootEvalPath,
   107  		parent.Flags,
   108  		parent.ListOptions)
   109  	// copy our block tpyes
   110  	child.BlockTypes = parent.BlockTypes
   111  	// set the child's parent
   112  	child.ParentParseCtx = parent
   113  	// set the dependency config
   114  	child.DependencyConfig = NewDependencyConfig(modVersion)
   115  	// set variables if parent has any
   116  	if parent.Variables != nil {
   117  		childVars, ok := parent.Variables.DependencyVariables[modVersion.Name]
   118  		if ok {
   119  			child.Variables = childVars
   120  			child.Variables.PopulatePublicVariables()
   121  			child.AddVariablesToEvalContext()
   122  		}
   123  	}
   124  
   125  	return child
   126  }
   127  
   128  func (m *ModParseContext) EnsureWorkspaceLock(mod *modconfig.Mod) error {
   129  	// if the mod has dependencies, there must a workspace lock object in the run context
   130  	// (mod MUST be the workspace mod, not a dependency, as we would hit this error as soon as we parse it)
   131  	if mod.HasDependentMods() && (m.WorkspaceLock.Empty() || m.WorkspaceLock.Incomplete()) {
   132  		return fmt.Errorf("not all dependencies are installed - run 'steampipe mod install'")
   133  	}
   134  
   135  	return nil
   136  }
   137  
   138  func (m *ModParseContext) PushParent(parent modconfig.ModTreeItem) {
   139  	m.parents = append(m.parents, parent.GetUnqualifiedName())
   140  }
   141  
   142  func (m *ModParseContext) PopParent() string {
   143  	n := len(m.parents) - 1
   144  	res := m.parents[n]
   145  	m.parents = m.parents[:n]
   146  	return res
   147  }
   148  
   149  func (m *ModParseContext) PeekParent() string {
   150  	if len(m.parents) == 0 {
   151  		return m.CurrentMod.Name()
   152  	}
   153  	return m.parents[len(m.parents)-1]
   154  }
   155  
   156  // VariableValueCtyMap converts a map of variables to a map of the underlying cty value
   157  func VariableValueCtyMap(variables map[string]*modconfig.Variable) map[string]cty.Value {
   158  	ret := make(map[string]cty.Value, len(variables))
   159  	for k, v := range variables {
   160  		ret[k] = v.Value
   161  	}
   162  	return ret
   163  }
   164  
   165  // AddInputVariableValues adds evaluated variables to the run context.
   166  // This function is called for the root run context after loading all input variables
   167  func (m *ModParseContext) AddInputVariableValues(inputVariables *modconfig.ModVariableMap) {
   168  	// store the variables
   169  	m.Variables = inputVariables
   170  
   171  	// now add variables into eval context
   172  	m.AddVariablesToEvalContext()
   173  }
   174  
   175  func (m *ModParseContext) AddVariablesToEvalContext() {
   176  	m.addRootVariablesToReferenceMap()
   177  	m.addDependencyVariablesToReferenceMap()
   178  	m.buildEvalContext()
   179  }
   180  
   181  // addRootVariablesToReferenceMap sets the Variables property
   182  // and adds the variables to the referenceValues map (used to build the eval context)
   183  func (m *ModParseContext) addRootVariablesToReferenceMap() {
   184  
   185  	variables := m.Variables.RootVariables
   186  	// write local variables directly into referenceValues map
   187  	// NOTE: we add with the name "var" not "variable" as that is how variables are referenced
   188  	m.referenceValues["local"]["var"] = VariableValueCtyMap(variables)
   189  }
   190  
   191  // addDependencyVariablesToReferenceMap adds the dependency variables to the referenceValues map
   192  // (used to build the eval context)
   193  func (m *ModParseContext) addDependencyVariablesToReferenceMap() {
   194  	// retrieve the resolved dependency versions for the parent mod
   195  	resolvedVersions := m.WorkspaceLock.InstallCache[m.Variables.Mod.GetInstallCacheKey()]
   196  
   197  	for depModName, depVars := range m.Variables.DependencyVariables {
   198  		alias := resolvedVersions[depModName].Alias
   199  		if m.referenceValues[alias] == nil {
   200  			m.referenceValues[alias] = make(ReferenceTypeValueMap)
   201  		}
   202  		m.referenceValues[alias]["var"] = VariableValueCtyMap(depVars.RootVariables)
   203  	}
   204  }
   205  
   206  // AddModResources is used to add mod resources to the eval context
   207  func (m *ModParseContext) AddModResources(mod *modconfig.Mod) hcl.Diagnostics {
   208  	if len(m.UnresolvedBlocks) > 0 {
   209  		// should never happen
   210  		panic("calling AddModResources on ModParseContext but there are unresolved blocks from a previous parse")
   211  	}
   212  
   213  	var diags hcl.Diagnostics
   214  
   215  	moreDiags := m.storeResourceInReferenceValueMap(mod)
   216  	diags = append(diags, moreDiags...)
   217  
   218  	// do not add variables (as they have already been added)
   219  	// if the resource is for a dependency mod, do not add locals
   220  	shouldAdd := func(item modconfig.HclResource) bool {
   221  		if item.BlockType() == modconfig.BlockTypeVariable ||
   222  			item.BlockType() == modconfig.BlockTypeLocals && item.(modconfig.ModTreeItem).GetMod().ShortName != m.CurrentMod.ShortName {
   223  			return false
   224  		}
   225  		return true
   226  	}
   227  
   228  	resourceFunc := func(item modconfig.HclResource) (bool, error) {
   229  		// add all mod resources (except those excluded) into cty map
   230  		if shouldAdd(item) {
   231  			moreDiags := m.storeResourceInReferenceValueMap(item)
   232  			diags = append(diags, moreDiags...)
   233  		}
   234  		// continue walking
   235  		return true, nil
   236  	}
   237  	mod.WalkResources(resourceFunc)
   238  
   239  	// rebuild the eval context
   240  	m.buildEvalContext()
   241  	return diags
   242  }
   243  
   244  func (m *ModParseContext) SetDecodeContent(content *hcl.BodyContent, fileData map[string][]byte) {
   245  	// put blocks into map as well
   246  	m.topLevelBlocks = make(map[*hcl.Block]struct{}, len(m.blocks))
   247  	for _, b := range content.Blocks {
   248  		m.topLevelBlocks[b] = struct{}{}
   249  	}
   250  	m.ParseContext.SetDecodeContent(content, fileData)
   251  }
   252  
   253  // AddDependencies :: the block could not be resolved as it has dependencies
   254  // 1) store block as unresolved
   255  // 2) add dependencies to our tree of dependencies
   256  func (m *ModParseContext) AddDependencies(block *hcl.Block, name string, dependencies map[string]*modconfig.ResourceDependency) hcl.Diagnostics {
   257  	// TACTICAL if this is NOT a top level block, add a suffix to the block name
   258  	// this is needed to avoid circular dependency errors if a nested block references
   259  	// a top level block with the same name
   260  	if !m.IsTopLevelBlock(block) {
   261  		name = "nested." + name
   262  	}
   263  	return m.ParseContext.AddDependencies(block, name, dependencies)
   264  }
   265  
   266  // ShouldCreateDefaultMod returns whether the flag is set to create a default mod if no mod definition exists
   267  func (m *ModParseContext) ShouldCreateDefaultMod() bool {
   268  	return m.Flags&CreateDefaultMod == CreateDefaultMod
   269  }
   270  
   271  // CreatePseudoResources returns whether the flag is set to create pseudo resources
   272  func (m *ModParseContext) CreatePseudoResources() bool {
   273  	return m.Flags&CreatePseudoResources == CreatePseudoResources
   274  }
   275  
   276  // AddResource stores this resource as a variable to be added to the eval context.
   277  func (m *ModParseContext) AddResource(resource modconfig.HclResource) hcl.Diagnostics {
   278  	diagnostics := m.storeResourceInReferenceValueMap(resource)
   279  	if diagnostics.HasErrors() {
   280  		return diagnostics
   281  	}
   282  
   283  	// rebuild the eval context
   284  	m.buildEvalContext()
   285  
   286  	return nil
   287  }
   288  
   289  // GetMod finds the mod with given short name, looking only in first level dependencies
   290  // this is used to resolve resource references
   291  // specifically when the 'children' property of dashboards and benchmarks refers to resource in a dependency mod
   292  func (m *ModParseContext) GetMod(modShortName string) *modconfig.Mod {
   293  	if modShortName == m.CurrentMod.ShortName {
   294  		return m.CurrentMod
   295  	}
   296  	// we need to iterate through dependency mods of the current mod
   297  	key := m.CurrentMod.GetInstallCacheKey()
   298  	deps := m.WorkspaceLock.InstallCache[key]
   299  	for _, dep := range deps {
   300  		depMod, ok := m.topLevelDependencyMods[dep.Name]
   301  		if ok && depMod.ShortName == modShortName {
   302  			return depMod
   303  		}
   304  	}
   305  	return nil
   306  }
   307  
   308  func (m *ModParseContext) GetResourceMaps() *modconfig.ResourceMaps {
   309  	// use the current mod as the base resource map
   310  	resourceMap := m.CurrentMod.GetResourceMaps()
   311  	// get a map of top level loaded dep mods
   312  	deps := m.GetTopLevelDependencyMods()
   313  
   314  	dependencyResourceMaps := make([]*modconfig.ResourceMaps, 0, len(deps))
   315  
   316  	// merge in the top level resources of the dependency mods
   317  	for _, dep := range deps {
   318  		dependencyResourceMaps = append(dependencyResourceMaps, dep.GetResourceMaps().TopLevelResources())
   319  	}
   320  
   321  	resourceMap = resourceMap.Merge(dependencyResourceMaps)
   322  	return resourceMap
   323  }
   324  
   325  func (m *ModParseContext) GetResource(parsedName *modconfig.ParsedResourceName) (resource modconfig.HclResource, found bool) {
   326  	return m.GetResourceMaps().GetResource(parsedName)
   327  }
   328  
   329  // build the eval context from the cached reference values
   330  func (m *ModParseContext) buildEvalContext() {
   331  	// convert reference values to cty objects
   332  	referenceValues := make(map[string]cty.Value)
   333  
   334  	// now for each mod add all the values
   335  	for mod, modMap := range m.referenceValues {
   336  		if mod == "local" {
   337  			for k, v := range modMap {
   338  				referenceValues[k] = cty.ObjectVal(v)
   339  			}
   340  			continue
   341  		}
   342  
   343  		// mod map is map[string]map[string]cty.Value
   344  		// for each element (i.e. map[string]cty.Value) convert to cty object
   345  		refTypeMap := make(map[string]cty.Value)
   346  		if mod == "local" {
   347  			for k, v := range modMap {
   348  				referenceValues[k] = cty.ObjectVal(v)
   349  			}
   350  		} else {
   351  			for refType, typeValueMap := range modMap {
   352  				refTypeMap[refType] = cty.ObjectVal(typeValueMap)
   353  			}
   354  		}
   355  		// now convert the referenceValues itself to a cty object
   356  		referenceValues[mod] = cty.ObjectVal(refTypeMap)
   357  	}
   358  
   359  	// rebuild the eval context
   360  	m.ParseContext.buildEvalContext(referenceValues)
   361  }
   362  
   363  // store the resource as a cty value in the reference valuemap
   364  func (m *ModParseContext) storeResourceInReferenceValueMap(resource modconfig.HclResource) hcl.Diagnostics {
   365  	// add resource to variable map
   366  	ctyValue, diags := m.getResourceCtyValue(resource)
   367  	if diags.HasErrors() {
   368  		return diags
   369  	}
   370  
   371  	// add into the reference value map
   372  	if diags := m.addReferenceValue(resource, ctyValue); diags.HasErrors() {
   373  		return diags
   374  	}
   375  
   376  	// remove this resource from unparsed blocks
   377  	delete(m.UnresolvedBlocks, resource.Name())
   378  
   379  	return nil
   380  }
   381  
   382  // convert a HclResource into a cty value, taking into account nested structs
   383  func (m *ModParseContext) getResourceCtyValue(resource modconfig.HclResource) (cty.Value, hcl.Diagnostics) {
   384  	ctyValue, err := resource.(modconfig.CtyValueProvider).CtyValue()
   385  	if err != nil {
   386  		return cty.Zero, m.errToCtyValueDiags(resource, err)
   387  	}
   388  	// if this is a value map, merge in the values of base structs
   389  	// if it is NOT a value map, the resource must have overridden CtyValue so do not merge base structs
   390  	if ctyValue.Type().FriendlyName() != "object" {
   391  		return ctyValue, nil
   392  	}
   393  	// TODO [node_reuse] fetch nested structs and serialise automatically https://github.com/turbot/steampipe/issues/2924
   394  	valueMap := ctyValue.AsValueMap()
   395  	if valueMap == nil {
   396  		valueMap = make(map[string]cty.Value)
   397  	}
   398  	base := resource.GetHclResourceImpl()
   399  	if err := m.mergeResourceCtyValue(base, valueMap); err != nil {
   400  		return cty.Zero, m.errToCtyValueDiags(resource, err)
   401  	}
   402  
   403  	if qp, ok := resource.(modconfig.QueryProvider); ok {
   404  		base := qp.GetQueryProviderImpl()
   405  		if err := m.mergeResourceCtyValue(base, valueMap); err != nil {
   406  			return cty.Zero, m.errToCtyValueDiags(resource, err)
   407  		}
   408  	}
   409  
   410  	if treeItem, ok := resource.(modconfig.ModTreeItem); ok {
   411  		base := treeItem.GetModTreeItemImpl()
   412  		if err := m.mergeResourceCtyValue(base, valueMap); err != nil {
   413  			return cty.Zero, m.errToCtyValueDiags(resource, err)
   414  		}
   415  	}
   416  	return cty.ObjectVal(valueMap), nil
   417  }
   418  
   419  // merge the cty value of the given interface into valueMap
   420  // (note: this mutates valueMap)
   421  func (m *ModParseContext) mergeResourceCtyValue(resource modconfig.CtyValueProvider, valueMap map[string]cty.Value) (err error) {
   422  	defer func() {
   423  		if r := recover(); r != nil {
   424  			err = fmt.Errorf("panic in mergeResourceCtyValue: %s", helpers.ToError(r).Error())
   425  		}
   426  	}()
   427  	ctyValue, err := resource.CtyValue()
   428  	if err != nil {
   429  		return err
   430  	}
   431  	if ctyValue == cty.Zero {
   432  		return nil
   433  	}
   434  	// merge results
   435  	for k, v := range ctyValue.AsValueMap() {
   436  		valueMap[k] = v
   437  	}
   438  	return nil
   439  }
   440  
   441  func (m *ModParseContext) errToCtyValueDiags(resource modconfig.HclResource, err error) hcl.Diagnostics {
   442  	return hcl.Diagnostics{&hcl.Diagnostic{
   443  		Severity: hcl.DiagError,
   444  		Summary:  fmt.Sprintf("failed to convert resource '%s' to its cty value", resource.Name()),
   445  		Detail:   err.Error(),
   446  		Subject:  resource.GetDeclRange(),
   447  	}}
   448  }
   449  
   450  func (m *ModParseContext) addReferenceValue(resource modconfig.HclResource, value cty.Value) hcl.Diagnostics {
   451  	parsedName, err := modconfig.ParseResourceName(resource.Name())
   452  	if err != nil {
   453  		return hcl.Diagnostics{&hcl.Diagnostic{
   454  			Severity: hcl.DiagError,
   455  			Summary:  fmt.Sprintf("failed to parse resource name %s", resource.Name()),
   456  			Detail:   err.Error(),
   457  			Subject:  resource.GetDeclRange(),
   458  		}}
   459  	}
   460  
   461  	// TODO validate mod name clashes
   462  	// TODO mod reserved names
   463  	// TODO handle aliases
   464  
   465  	key := parsedName.Name
   466  	typeString := parsedName.ItemType
   467  
   468  	// most resources will have a mod property - use this if available
   469  	var mod *modconfig.Mod
   470  	if modTreeItem, ok := resource.(modconfig.ModTreeItem); ok {
   471  		mod = modTreeItem.GetMod()
   472  	}
   473  	// fall back to current mod
   474  	if mod == nil {
   475  		mod = m.CurrentMod
   476  	}
   477  
   478  	modName := mod.ShortName
   479  	if mod.ModPath == m.RootEvalPath {
   480  		modName = "local"
   481  	}
   482  	variablesForMod, ok := m.referenceValues[modName]
   483  	// do we have a map of reference values for this dep mod?
   484  	if !ok {
   485  		// no - create one
   486  		variablesForMod = make(ReferenceTypeValueMap)
   487  		m.referenceValues[modName] = variablesForMod
   488  	}
   489  	// do we have a map of reference values for this type
   490  	variablesForType, ok := variablesForMod[typeString]
   491  	if !ok {
   492  		// no - create one
   493  		variablesForType = make(map[string]cty.Value)
   494  	}
   495  
   496  	// DO NOT update the cached cty values if the value already exists
   497  	// this can happen in the case of variables where we initialise the context with values read from file
   498  	// or passed on the command line,	// does this item exist in the map
   499  	if _, ok := variablesForType[key]; !ok {
   500  		variablesForType[key] = value
   501  		variablesForMod[typeString] = variablesForType
   502  		m.referenceValues[modName] = variablesForMod
   503  	}
   504  
   505  	return nil
   506  }
   507  
   508  func (m *ModParseContext) IsTopLevelBlock(block *hcl.Block) bool {
   509  	_, isTopLevel := m.topLevelBlocks[block]
   510  	return isTopLevel
   511  }
   512  
   513  func (m *ModParseContext) AddLoadedDependencyMod(mod *modconfig.Mod) {
   514  	m.topLevelDependencyMods[mod.DependencyName] = mod
   515  }
   516  
   517  // GetTopLevelDependencyMods build a mod map of top level loaded dependencies, keyed by mod name
   518  func (m *ModParseContext) GetTopLevelDependencyMods() modconfig.ModMap {
   519  	return m.topLevelDependencyMods
   520  }
   521  
   522  func (m *ModParseContext) SetCurrentMod(mod *modconfig.Mod) error {
   523  	m.CurrentMod = mod
   524  	// now we have the mod, load any arg values from the mod require - these will be passed to dependency mods
   525  	return m.loadModRequireArgs()
   526  }
   527  
   528  // when reloading a mod dependency tree to resolve require args values, this function is called after each mod is loaded
   529  // to load the require arg values and update the variable values
   530  func (m *ModParseContext) loadModRequireArgs() error {
   531  	//if we have not loaded variable definitions yet, do not load require args
   532  	if m.Variables == nil {
   533  		return nil
   534  	}
   535  
   536  	depModVarValues, err := inputvars.CollectVariableValuesFromModRequire(m.CurrentMod, m.WorkspaceLock)
   537  	if err != nil {
   538  		return err
   539  	}
   540  	if len(depModVarValues) == 0 {
   541  		return nil
   542  	}
   543  	// if any mod require args have an unknown value, we have failed to resolve them - raise an error
   544  	if err := m.validateModRequireValues(depModVarValues); err != nil {
   545  		return err
   546  	}
   547  	// now update the variables map with the input values
   548  	depModVarValues.SetVariableValues(m.Variables)
   549  
   550  	// now add  overridden variables into eval context - in case the root mod references any dependency variable values
   551  	m.AddVariablesToEvalContext()
   552  
   553  	return nil
   554  }
   555  
   556  func (m *ModParseContext) validateModRequireValues(depModVarValues inputvars.InputValues) error {
   557  	if len(depModVarValues) == 0 {
   558  		return nil
   559  	}
   560  	var missingVarExpressions []string
   561  	requireBlock := m.getModRequireBlock()
   562  	if requireBlock == nil {
   563  		return fmt.Errorf("require args extracted but no require block found for %s", m.CurrentMod.Name())
   564  	}
   565  
   566  	for k, v := range depModVarValues {
   567  		// if we successfully resolved this value, continue
   568  		if v.Value.IsKnown() {
   569  			continue
   570  		}
   571  		parsedVarName, err := modconfig.ParseResourceName(k)
   572  		if err != nil {
   573  			return err
   574  		}
   575  
   576  		// re-parse the require block manually to extract the range and unresolved arg value expression
   577  		var errorString string
   578  		errorString, err = m.getErrorStringForUnresolvedArg(parsedVarName, requireBlock)
   579  		if err != nil {
   580  			// if there was an error retrieving details, return less specific error string
   581  			errorString = fmt.Sprintf("\"%s\"  (%s %s)", k, m.CurrentMod.Name(), m.CurrentMod.GetDeclRange().Filename)
   582  		}
   583  
   584  		missingVarExpressions = append(missingVarExpressions, errorString)
   585  	}
   586  
   587  	if errorCount := len(missingVarExpressions); errorCount > 0 {
   588  		if errorCount == 1 {
   589  			return fmt.Errorf("failed to resolve dependency mod argument value: %s", missingVarExpressions[0])
   590  		}
   591  
   592  		return fmt.Errorf("failed to resolve %d dependency mod arguments %s:\n\t%s", errorCount, utils.Pluralize("value", errorCount), strings.Join(missingVarExpressions, "\n\t"))
   593  	}
   594  	return nil
   595  }
   596  
   597  func (m *ModParseContext) getErrorStringForUnresolvedArg(parsedVarName *modconfig.ParsedResourceName, requireBlock *hclsyntax.Block) (_ string, err error) {
   598  	defer func() {
   599  		if r := recover(); r != nil {
   600  			err = helpers.ToError(r)
   601  		}
   602  	}()
   603  	// which mod and variable is this is this for
   604  	modShortName := parsedVarName.Mod
   605  	varName := parsedVarName.Name
   606  	var modDependencyName string
   607  	// determine the mod dependency name as that is how it will be keyed in the require map
   608  	for depName, modVersion := range m.WorkspaceLock.InstallCache[m.CurrentMod.GetInstallCacheKey()] {
   609  		if modVersion.Alias == modShortName {
   610  			modDependencyName = depName
   611  			break
   612  		}
   613  	}
   614  
   615  	// iterate through require blocks looking for mod blocks
   616  	for _, b := range requireBlock.Body.Blocks {
   617  		// only interested in mod blocks
   618  		if b.Type != "mod" {
   619  			continue
   620  		}
   621  		// if this is not the mod we're looking for, continue
   622  		if b.Labels[0] != modDependencyName {
   623  			continue
   624  		}
   625  		// now find the failed arg
   626  		argsAttr, ok := b.Body.Attributes["args"]
   627  		if !ok {
   628  			return "", fmt.Errorf("no args block found for %s", modDependencyName)
   629  		}
   630  		// iterate over args looking for the correctly named item
   631  		for _, a := range argsAttr.Expr.(*hclsyntax.ObjectConsExpr).Items {
   632  			thisVarName, err := a.KeyExpr.Value(&hcl.EvalContext{})
   633  			if err != nil {
   634  				return "", err
   635  			}
   636  
   637  			// is this the var we are looking for?
   638  			if thisVarName.AsString() != varName {
   639  				continue
   640  			}
   641  
   642  			// this is the var, get the value expression
   643  			expr, ok := a.ValueExpr.(*hclsyntax.ScopeTraversalExpr)
   644  			if !ok {
   645  				return "", fmt.Errorf("failed to get args details for %s", parsedVarName.ToResourceName())
   646  			}
   647  			// ok we have the expression - build the error string
   648  			exprString := hclhelpers.TraversalAsString(expr.Traversal)
   649  			r := expr.Range()
   650  			sourceRange := fmt.Sprintf("%s:%d", r.Filename, r.Start.Line)
   651  			res := fmt.Sprintf("\"%s = %s\" (%s %s)",
   652  				parsedVarName.ToResourceName(),
   653  				exprString,
   654  				m.CurrentMod.Name(),
   655  				sourceRange)
   656  			return res, nil
   657  
   658  		}
   659  	}
   660  	return "", fmt.Errorf("failed to get args details for %s", parsedVarName.ToResourceName())
   661  }
   662  
   663  func (m *ModParseContext) getModRequireBlock() *hclsyntax.Block {
   664  	for _, b := range m.CurrentMod.ResourceWithMetadataBaseRemain.(*hclsyntax.Body).Blocks {
   665  		if b.Type == modconfig.BlockTypeRequire {
   666  			return b
   667  		}
   668  	}
   669  	return nil
   670  
   671  }