github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/steampipeconfig/load_config.go (about) 1 package steampipeconfig 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "log" 8 "os" 9 "path/filepath" 10 "strings" 11 "time" 12 13 "github.com/gertd/go-pluralize" 14 "github.com/hashicorp/hcl/v2" 15 filehelpers "github.com/turbot/go-kit/files" 16 "github.com/turbot/go-kit/helpers" 17 "github.com/turbot/pipe-fittings/hclhelpers" 18 "github.com/turbot/steampipe-plugin-sdk/v5/sperr" 19 "github.com/turbot/steampipe/pkg/constants" 20 "github.com/turbot/steampipe/pkg/db/db_common" 21 "github.com/turbot/steampipe/pkg/error_helpers" 22 "github.com/turbot/steampipe/pkg/filepaths" 23 "github.com/turbot/steampipe/pkg/ociinstaller/versionfile" 24 "github.com/turbot/steampipe/pkg/steampipeconfig/modconfig" 25 "github.com/turbot/steampipe/pkg/steampipeconfig/options" 26 "github.com/turbot/steampipe/pkg/steampipeconfig/parse" 27 "github.com/turbot/steampipe/pkg/utils" 28 ) 29 30 var GlobalConfig *SteampipeConfig 31 var defaultConfigFileName = "default.spc" 32 var defaultConfigSampleFileName = "default.spc.sample" 33 34 // LoadSteampipeConfig loads the HCL connection config and workspace options 35 func LoadSteampipeConfig(ctx context.Context, modLocation string, commandName string) (*SteampipeConfig, error_helpers.ErrorAndWarnings) { 36 utils.LogTime("steampipeconfig.LoadSteampipeConfig start") 37 defer utils.LogTime("steampipeconfig.LoadSteampipeConfig end") 38 39 log.Printf("[INFO] ensureDefaultConfigFile") 40 41 if err := ensureDefaultConfigFile(filepaths.EnsureConfigDir()); err != nil { 42 return nil, error_helpers.NewErrorsAndWarning( 43 sperr.WrapWithMessage( 44 err, 45 "could not create default config", 46 ), 47 ) 48 } 49 return loadSteampipeConfig(ctx, modLocation, commandName) 50 } 51 52 // LoadConnectionConfig loads the connection config but not the workspace options 53 // this is called by the fdw 54 func LoadConnectionConfig(ctx context.Context) (*SteampipeConfig, error_helpers.ErrorAndWarnings) { 55 return LoadSteampipeConfig(ctx, "", "") 56 } 57 58 func ensureDefaultConfigFile(configFolder string) error { 59 // get the filepaths 60 defaultConfigFile := filepath.Join(configFolder, defaultConfigFileName) 61 defaultConfigSampleFile := filepath.Join(configFolder, defaultConfigSampleFileName) 62 63 // check if sample and default files exist 64 sampleExists := filehelpers.FileExists(defaultConfigSampleFile) 65 defaultExists := filehelpers.FileExists(defaultConfigFile) 66 67 var sampleContent []byte 68 var sampleModTime, defaultModTime time.Time 69 70 // if the sample file exists, load content and read mod time 71 if sampleExists { 72 sampleStat, err := os.Stat(defaultConfigSampleFile) 73 if err != nil { 74 return err 75 } 76 sampleContent, err = os.ReadFile(defaultConfigSampleFile) 77 if err != nil { 78 return err 79 } 80 sampleModTime = sampleStat.ModTime() 81 } 82 83 // if the default file exists read mod time 84 if defaultExists { 85 // get the file infos 86 defaultStat, err := os.Stat(defaultConfigFile) 87 if err != nil { 88 return err 89 } 90 // get the file mod times 91 defaultModTime = defaultStat.ModTime() 92 } 93 94 // check if the files are modified 95 96 // has the user modified the default file? 97 userModifiedDefault := defaultModTime.IsZero() || 98 defaultModTime.After(sampleModTime) && defaultModTime.Sub(sampleModTime) > 100*time.Millisecond 99 100 // has the DefaultConnectionConfigContent been updated since the sample file was last writtne 101 sampleModified := sampleModTime.IsZero() || 102 !bytes.Equal([]byte(constants.DefaultConnectionConfigContent), sampleContent) 103 104 // case: if sample is modified - always write new sample file content 105 if sampleModified { 106 err := os.WriteFile(defaultConfigSampleFile, []byte(constants.DefaultConnectionConfigContent), 0755) 107 if err != nil { 108 return err 109 } 110 } 111 112 // case: if sample is modified but default is not modified - write the new default file content 113 if sampleModified && !userModifiedDefault { 114 err := os.WriteFile(defaultConfigFile, []byte(constants.DefaultConnectionConfigContent), 0755) 115 if err != nil { 116 return err 117 } 118 } 119 return nil 120 } 121 122 func loadSteampipeConfig(ctx context.Context, modLocation string, commandName string) (steampipeConfig *SteampipeConfig, errorsAndWarnings error_helpers.ErrorAndWarnings) { 123 utils.LogTime("steampipeconfig.loadSteampipeConfig start") 124 defer utils.LogTime("steampipeconfig.loadSteampipeConfig end") 125 126 errorsAndWarnings = error_helpers.NewErrorsAndWarning(nil) 127 defer func() { 128 if r := recover(); r != nil { 129 errorsAndWarnings = error_helpers.NewErrorsAndWarning(helpers.ToError(r)) 130 } 131 }() 132 133 steampipeConfig = NewSteampipeConfig(commandName) 134 135 // load plugin versions 136 v, err := versionfile.LoadPluginVersionFile(ctx) 137 if err != nil { 138 return nil, error_helpers.NewErrorsAndWarning(err) 139 } 140 141 // add any "local" plugins (i.e. plugins installed under the 'local' folder) into the version file 142 ew := v.AddLocalPlugins(ctx) 143 if ew.GetError() != nil { 144 return nil, ew 145 } 146 steampipeConfig.PluginVersions = v.Plugins 147 148 // load config from the installation folder - load all spc files from config directory 149 include := filehelpers.InclusionsFromExtensions(constants.ConnectionConfigExtensions) 150 loadOptions := &loadConfigOptions{include: include} 151 ew = loadConfig(ctx, filepaths.EnsureConfigDir(), steampipeConfig, loadOptions) 152 if ew.GetError() != nil { 153 return nil, ew 154 } 155 // merge the warning from this call 156 errorsAndWarnings.AddWarning(ew.Warnings...) 157 158 // now load config from the workspace folder, if provided 159 // this has precedence and so will overwrite any config which has already been set 160 // check workspace folder exists 161 if modLocation != "" { 162 if _, err := os.Stat(modLocation); os.IsNotExist(err) { 163 return nil, error_helpers.NewErrorsAndWarning(fmt.Errorf("mod location '%s' does not exist", modLocation)) 164 } 165 166 // only include workspace.spc from workspace directory 167 include = filehelpers.InclusionsFromFiles([]string{filepaths.WorkspaceConfigFileName}) 168 // update load options to ONLY allow terminal options 169 loadOptions = &loadConfigOptions{include: include, allowedOptions: []string{options.TerminalBlock}} 170 ew := loadConfig(ctx, modLocation, steampipeConfig, loadOptions) 171 if ew.GetError() != nil { 172 return nil, ew.WrapErrorWithMessage("failed to load workspace config") 173 } 174 175 // merge the warning from this call 176 errorsAndWarnings.AddWarning(ew.Warnings...) 177 } 178 179 // now set default options on all connections without options set 180 // this is needed as the connection config is also loaded by the FDW which has no access to viper 181 steampipeConfig.setDefaultConnectionOptions() 182 183 // now validate the config 184 warnings, errors := steampipeConfig.Validate() 185 logValidationResult(warnings, errors) 186 187 return steampipeConfig, errorsAndWarnings 188 } 189 190 func logValidationResult(warnings []string, errors []string) { 191 if len(warnings) > 0 { 192 error_helpers.ShowWarning(buildValidationLogString(warnings, "warning")) 193 log.Printf("[TRACE] %s", buildValidationLogString(warnings, "warning")) 194 } 195 if len(errors) > 0 { 196 error_helpers.ShowWarning(buildValidationLogString(errors, "error")) 197 log.Printf("[TRACE] %s", buildValidationLogString(errors, "error")) 198 } 199 } 200 201 func buildValidationLogString(items []string, validationType string) string { 202 count := len(items) 203 if count == 0 { 204 return "" 205 } 206 var str strings.Builder 207 str.WriteString(fmt.Sprintf("connection config has has %d validation %s:\n", 208 count, 209 pluralize.NewClient().Pluralize(validationType, count, false), 210 )) 211 for _, w := range items { 212 str.WriteString(fmt.Sprintf("\t %s\n", w)) 213 } 214 return str.String() 215 } 216 217 // load config from the given folder and update steampipeConfig 218 // NOTE: this mutates steampipe config 219 type loadConfigOptions struct { 220 include []string 221 allowedOptions []string 222 } 223 224 func loadConfig(ctx context.Context, configFolder string, steampipeConfig *SteampipeConfig, opts *loadConfigOptions) error_helpers.ErrorAndWarnings { 225 log.Printf("[INFO] loadConfig is loading connection config") 226 // get all the config files in the directory 227 configPaths, err := filehelpers.ListFilesWithContext(ctx, configFolder, &filehelpers.ListOptions{ 228 Flags: filehelpers.FilesFlat, 229 Include: opts.include, 230 }) 231 232 if err != nil { 233 log.Printf("[WARN] loadConfig: failed to get config file paths: %v\n", err) 234 return error_helpers.NewErrorsAndWarning(err) 235 } 236 if len(configPaths) == 0 { 237 return error_helpers.ErrorAndWarnings{} 238 } 239 240 fileData, diags := parse.LoadFileData(configPaths...) 241 if diags.HasErrors() { 242 log.Printf("[WARN] loadConfig: failed to load all config files: %v\n", err) 243 return error_helpers.DiagsToErrorsAndWarnings("Failed to load all config files", diags) 244 } 245 246 body, diags := parse.ParseHclFiles(fileData) 247 if diags.HasErrors() { 248 return error_helpers.DiagsToErrorsAndWarnings("Failed to load all config files", diags) 249 } 250 251 // do a partial decode 252 content, moreDiags := body.Content(parse.ConfigBlockSchema) 253 if moreDiags.HasErrors() { 254 diags = append(diags, moreDiags...) 255 return error_helpers.DiagsToErrorsAndWarnings("Failed to load config", diags) 256 } 257 258 // store block types which we have found in this folder - each is only allowed once 259 // NOTE this is different to merging options with options already populated in the passed-in steampipe config 260 // this is valid because the same block may be defined in the config folder and the workspace 261 optionBlockMap := map[string]bool{} 262 263 for _, block := range content.Blocks { 264 switch block.Type { 265 266 case modconfig.BlockTypePlugin: 267 plugin, moreDiags := parse.DecodePlugin(block) 268 diags = append(diags, moreDiags...) 269 if moreDiags.HasErrors() { 270 continue 271 } 272 // add plugin to steampipeConfig 273 // NOTE: this errors if there is a plugin block with a duplicate label 274 if err := steampipeConfig.addPlugin(plugin); err != nil { 275 return error_helpers.NewErrorsAndWarning(err) 276 } 277 278 case modconfig.BlockTypeConnection: 279 connection, moreDiags := parse.DecodeConnection(block) 280 diags = append(diags, moreDiags...) 281 if moreDiags.HasErrors() { 282 continue 283 } 284 if existingConnection, alreadyThere := steampipeConfig.Connections[connection.Name]; alreadyThere { 285 err := getDuplicateConnectionError(existingConnection, connection) 286 return error_helpers.NewErrorsAndWarning(err) 287 } 288 if ok, errorMessage := db_common.IsSchemaNameValid(connection.Name); !ok { 289 return error_helpers.NewErrorsAndWarning(sperr.New("invalid connection name: '%s' in '%s'. %s ", connection.Name, block.TypeRange.Filename, errorMessage)) 290 } 291 steampipeConfig.Connections[connection.Name] = connection 292 293 case modconfig.BlockTypeOptions: 294 // check this options type is permitted based on the options passed in 295 if err := optionsBlockPermitted(block, optionBlockMap, opts); err != nil { 296 return error_helpers.NewErrorsAndWarning(err) 297 } 298 opts, moreDiags := parse.DecodeOptions(block) 299 if moreDiags.HasErrors() { 300 diags = append(diags, moreDiags...) 301 continue 302 } 303 // set options on steampipe config 304 // if options are already set, this will merge the new options over the top of the existing options 305 // i.e. new options have precedence 306 e := steampipeConfig.SetOptions(opts) 307 if e.GetError() != nil { 308 // we should never get an error here, since SetOptions 309 // only sets warnings 310 // putting this here only for good-practice 311 return e 312 } 313 if len(e.Warnings) > 0 { 314 for _, warning := range e.Warnings { 315 diags = append(diags, &hcl.Diagnostic{ 316 Severity: hcl.DiagWarning, 317 Summary: warning, 318 Subject: hclhelpers.BlockRangePointer(block), 319 }) 320 } 321 } 322 } 323 } 324 325 if diags.HasErrors() { 326 return error_helpers.DiagsToErrorsAndWarnings("Failed to load config", diags) 327 } 328 329 res := error_helpers.DiagsToErrorsAndWarnings("", diags) 330 331 log.Printf("[INFO] loadConfig calling initializePlugins") 332 333 // resolve the plugins for each connection and create default plugin config 334 // for all plugins mentioned in connection config which have no explicit config 335 steampipeConfig.initializePlugins() 336 337 return res 338 } 339 340 func getDuplicateConnectionError(existingConnection, newConnection *modconfig.Connection) error { 341 return sperr.New("duplicate connection name: '%s'\n\t(%s:%d)\n\t(%s:%d)", 342 existingConnection.Name, existingConnection.DeclRange.Filename, existingConnection.DeclRange.Start.Line, 343 newConnection.DeclRange.Filename, newConnection.DeclRange.Start.Line) 344 } 345 346 func optionsBlockPermitted(block *hcl.Block, blockMap map[string]bool, opts *loadConfigOptions) error { 347 // keep track of duplicate block types 348 blockType := block.Labels[0] 349 if _, ok := blockMap[blockType]; ok { 350 return fmt.Errorf("multiple instances of '%s' options block", blockType) 351 } 352 blockMap[blockType] = true 353 permitted := len(opts.allowedOptions) == 0 || 354 helpers.StringSliceContains(opts.allowedOptions, blockType) 355 356 if !permitted { 357 return fmt.Errorf("'%s' options block is not permitted", blockType) 358 } 359 return nil 360 }