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 }