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

     1  package steampipeconfig
     2  
     3  import (
     4  	"context"
     5  	"golang.org/x/exp/maps"
     6  	"log"
     7  	"sort"
     8  	"strings"
     9  
    10  	"github.com/spf13/viper"
    11  	"github.com/turbot/steampipe-plugin-sdk/v5/plugin"
    12  	"github.com/turbot/steampipe/pkg/constants"
    13  	"github.com/turbot/steampipe/pkg/error_helpers"
    14  	"github.com/turbot/steampipe/pkg/steampipeconfig/inputvars"
    15  	"github.com/turbot/steampipe/pkg/steampipeconfig/modconfig"
    16  	"github.com/turbot/steampipe/pkg/steampipeconfig/parse"
    17  	"github.com/turbot/steampipe/pkg/steampipeconfig/versionmap"
    18  	"github.com/turbot/steampipe/pkg/utils"
    19  	"github.com/turbot/terraform-components/tfdiags"
    20  )
    21  
    22  func LoadVariableDefinitions(ctx context.Context, variablePath string, parseCtx *parse.ModParseContext) (*modconfig.ModVariableMap, error) {
    23  	// only load mod and variables blocks
    24  	parseCtx.BlockTypes = []string{modconfig.BlockTypeVariable}
    25  	mod, errAndWarnings := LoadMod(ctx, variablePath, parseCtx)
    26  	if errAndWarnings.GetError() != nil {
    27  		return nil, errAndWarnings.GetError()
    28  	}
    29  
    30  	variableMap := modconfig.NewModVariableMap(mod)
    31  
    32  	return variableMap, nil
    33  }
    34  
    35  func GetVariableValues(parseCtx *parse.ModParseContext, variableMap *modconfig.ModVariableMap, validate bool) (*modconfig.ModVariableMap, error_helpers.ErrorAndWarnings) {
    36  	log.Printf("[INFO] GetVariableValues")
    37  	// now resolve all input variables
    38  	inputValues, errorsAndWarnings := getInputVariables(parseCtx, variableMap, validate)
    39  	if errorsAndWarnings.Error == nil {
    40  		// now update the variables map with the input values
    41  		inputValues.SetVariableValues(variableMap)
    42  	}
    43  
    44  	return variableMap, errorsAndWarnings
    45  }
    46  
    47  func getInputVariables(parseCtx *parse.ModParseContext, variableMap *modconfig.ModVariableMap, validate bool) (inputvars.InputValues, error_helpers.ErrorAndWarnings) {
    48  	variableFileArgs := viper.GetStringSlice(constants.ArgVarFile)
    49  	variableArgs := viper.GetStringSlice(constants.ArgVariable)
    50  
    51  	// get mod and mod path from run context
    52  	mod := parseCtx.CurrentMod
    53  	path := mod.ModPath
    54  
    55  	log.Printf("[INFO] getInputVariables, variableFileArgs: %s, variableArgs: %s", variableFileArgs, variableArgs)
    56  
    57  	var inputValuesUnparsed, err = inputvars.CollectVariableValues(path, variableFileArgs, variableArgs, parseCtx.CurrentMod)
    58  	if err != nil {
    59  		log.Printf("[WARN] CollectVariableValues failed: %s", err.Error())
    60  
    61  		return nil, error_helpers.NewErrorsAndWarning(err)
    62  	}
    63  
    64  	log.Printf("[INFO] collected unparsed input values for vars: %s", strings.Join(maps.Keys(inputValuesUnparsed), ","))
    65  
    66  	if validate {
    67  		if err := identifyAllMissingVariables(parseCtx, variableMap, inputValuesUnparsed); err != nil {
    68  			log.Printf("[INFO] identifyAllMissingVariables returned a validation error: %s", err.Error())
    69  
    70  			return nil, error_helpers.NewErrorsAndWarning(err)
    71  		}
    72  	}
    73  
    74  	// only parse values for public variables
    75  	parsedValues, diags := inputvars.ParseVariableValues(inputValuesUnparsed, variableMap, validate)
    76  	if diags.HasErrors() {
    77  		log.Printf("[INFO] ParseVariableValues returned error: %s", diags.Err())
    78  	} else {
    79  		log.Printf("[INFO] parsed values for public variables: %s", strings.Join(maps.Keys(parsedValues), ","))
    80  	}
    81  
    82  	if validate {
    83  		moreDiags := inputvars.CheckInputVariables(variableMap.PublicVariables, parsedValues)
    84  		diags = append(diags, moreDiags...)
    85  	}
    86  
    87  	return parsedValues, newVariableValidationResult(diags)
    88  }
    89  
    90  func newVariableValidationResult(diags tfdiags.Diagnostics) error_helpers.ErrorAndWarnings {
    91  	warnings := plugin.DiagsToWarnings(diags.ToHCL())
    92  	var err error
    93  	if diags.HasErrors() {
    94  		err = newVariableValidationFailedError(diags)
    95  	}
    96  	return error_helpers.NewErrorsAndWarning(err, warnings...)
    97  }
    98  
    99  func identifyAllMissingVariables(parseCtx *parse.ModParseContext, variableMap *modconfig.ModVariableMap, variableValues map[string]inputvars.UnparsedVariableValue) error {
   100  	// convert variableValues into a lookup
   101  	var variableValueLookup = utils.SliceToLookup(maps.Keys(variableValues))
   102  	missingVarsMap, err := identifyMissingVariablesForDependencies(parseCtx.WorkspaceLock, variableMap, variableValueLookup, nil)
   103  
   104  	if err != nil {
   105  		return err
   106  	}
   107  	if len(missingVarsMap) == 0 {
   108  		// all good
   109  		return nil
   110  	}
   111  
   112  	// build a MissingVariableError
   113  	missingVarErr := NewMissingVarsError(parseCtx.CurrentMod)
   114  
   115  	// build a lookup with the dependency path of the root mod and all top level dependencies
   116  	rootName := variableMap.Mod.ShortName
   117  	topLevelModLookup := map[DependencyPathKey]struct{}{DependencyPathKey(rootName): {}}
   118  	for dep := range parseCtx.WorkspaceLock.InstallCache {
   119  		depPathKey := newDependencyPathKey(rootName, dep)
   120  		topLevelModLookup[depPathKey] = struct{}{}
   121  	}
   122  	for depPath, missingVars := range missingVarsMap {
   123  		if _, isTopLevel := topLevelModLookup[depPath]; isTopLevel {
   124  			missingVarErr.MissingVariables = append(missingVarErr.MissingVariables, missingVars...)
   125  		} else {
   126  			missingVarErr.MissingTransitiveVariables[depPath] = missingVars
   127  		}
   128  	}
   129  
   130  	return missingVarErr
   131  }
   132  
   133  func identifyMissingVariablesForDependencies(workspaceLock *versionmap.WorkspaceLock, variableMap *modconfig.ModVariableMap, parentVariableValuesLookup map[string]struct{}, dependencyPath []string) (map[DependencyPathKey][]*modconfig.Variable, error) {
   134  	// return a map of missing variables, keyed by dependency path
   135  	res := make(map[DependencyPathKey][]*modconfig.Variable)
   136  
   137  	// update the path to this dependency
   138  	dependencyPath = append(dependencyPath, variableMap.Mod.GetInstallCacheKey())
   139  
   140  	// clone variableValuesLookup so we can mutate it with depdency specific args overrides
   141  	var variableValueLookup = make(map[string]struct{}, len(parentVariableValuesLookup))
   142  	for k := range parentVariableValuesLookup {
   143  		// convert the variable name to the short name if it is fully qualified and belongs to the current mod
   144  		k = getVariableValueMapKey(k, variableMap)
   145  
   146  		variableValueLookup[k] = struct{}{}
   147  	}
   148  
   149  	// first get any args specified in the mod requires
   150  	// note the actual value of these may be unknown as we have not yet resolved
   151  	depModArgs, err := inputvars.CollectVariableValuesFromModRequire(variableMap.Mod, workspaceLock)
   152  	for varName := range depModArgs {
   153  		// convert the variable name to the short name if it is fully qualified and belongs to the current mod
   154  		varName = getVariableValueMapKey(varName, variableMap)
   155  
   156  		variableValueLookup[varName] = struct{}{}
   157  	}
   158  	if err != nil {
   159  		return nil, err
   160  	}
   161  
   162  	//  handle root variables
   163  	missingVariables := identifyMissingVariables(variableMap.RootVariables, variableValueLookup, variableMap.Mod.ShortName)
   164  	if len(missingVariables) > 0 {
   165  		res[newDependencyPathKey(dependencyPath...)] = missingVariables
   166  	}
   167  
   168  	// now iterate through all the dependency variable maps
   169  	for _, dependencyVariableMap := range variableMap.DependencyVariables {
   170  		childMissingMap, err := identifyMissingVariablesForDependencies(workspaceLock, dependencyVariableMap, variableValueLookup, dependencyPath)
   171  		if err != nil {
   172  			return nil, err
   173  		}
   174  		// add results into map
   175  		for k, v := range childMissingMap {
   176  			res[k] = v
   177  		}
   178  	}
   179  	return res, nil
   180  }
   181  
   182  // getVariableValueMapKey checks whether the variable is fully qualified and belongs to the current mod,
   183  // if so use the short name
   184  func getVariableValueMapKey(k string, variableMap *modconfig.ModVariableMap) string {
   185  	// attempt to parse the variable name.
   186  	// Note: if the variable is not fully qualified (e.g. "var_name"),  ParseResourceName will return an error
   187  	// in which case we add it to our map unchanged
   188  	parsedName, err := modconfig.ParseResourceName(k)
   189  	// if this IS a dependency variable, the parse will success
   190  	// if the mod name is the same as the current mod (variableMap.Mod)
   191  	// then add a map entry with the variable short name
   192  	// this will allow us to match the variable value to a variable defined in this mod
   193  	if err == nil && parsedName.Mod == variableMap.Mod.ShortName {
   194  		k = parsedName.Name
   195  	}
   196  	return k
   197  }
   198  
   199  func identifyMissingVariables(variableMap map[string]*modconfig.Variable, variableValuesLookup map[string]struct{}, modName string) []*modconfig.Variable {
   200  
   201  	var needed []*modconfig.Variable
   202  
   203  	for shortName, v := range variableMap {
   204  		if !v.Required() {
   205  			continue // We only prompt for required variables
   206  		}
   207  		_, unparsedValExists := variableValuesLookup[shortName]
   208  
   209  		if !unparsedValExists {
   210  			needed = append(needed, v)
   211  		}
   212  	}
   213  	sort.SliceStable(needed, func(i, j int) bool {
   214  		return needed[i].Name() < needed[j].Name()
   215  	})
   216  	return needed
   217  
   218  }