diff --git a/cmd/check.go b/cmd/check.go index 2e3afd6c..8fa0b1b4 100644 --- a/cmd/check.go +++ b/cmd/check.go @@ -21,11 +21,7 @@ import ( "github.com/turbot/steampipe/control/controldisplay" "github.com/turbot/steampipe/control/controlexecute" "github.com/turbot/steampipe/control/controlhooks" - "github.com/turbot/steampipe/db/db_client" - "github.com/turbot/steampipe/db/db_common" - "github.com/turbot/steampipe/db/db_local" "github.com/turbot/steampipe/display" - "github.com/turbot/steampipe/modinstaller" "github.com/turbot/steampipe/statushooks" "github.com/turbot/steampipe/utils" "github.com/turbot/steampipe/workspace" @@ -80,7 +76,7 @@ You may specify one or more benchmarks or controls to run (separated by a space) AddStringArrayFlag(constants.ArgVariable, "", nil, "Specify the value of a variable"). AddStringFlag(constants.ArgWhere, "", "", "SQL 'where' clause, or named query, used to filter controls (cannot be used with '--tag')"). AddIntFlag(constants.ArgMaxParallel, "", constants.DefaultMaxConnections, "The maximum number of parallel executions", cmdconfig.FlagOptions.Hidden()). - AddBoolFlag(constants.ArgModInstall, "", true, "Specify whether to install mod depdencies before running the check") + AddBoolFlag(constants.ArgModInstall, "", true, "Specify whether to install mod dependencies before running the check") return cmd } @@ -219,94 +215,10 @@ func initialiseCheck(ctx context.Context) *control.InitData { statushooks.SetStatus(ctx, "Initializing...") defer statushooks.Done(ctx) - initData := &control.InitData{ - Result: &db_common.InitResult{}, - } - - if err := controldisplay.EnsureTemplates(); err != nil { - initData.Result.Error = err - return initData - } - - if viper.GetBool(constants.ArgModInstall) { - opts := &modinstaller.InstallOpts{WorkspacePath: viper.GetString(constants.ArgWorkspaceChDir)} - _, err := modinstaller.InstallWorkspaceDependencies(opts) - if err != nil { - initData.Result.Error = err - return initData - } - } - - if viper.GetString(constants.ArgOutput) == constants.CheckOutputFormatNone { - // set progress to false - viper.Set(constants.ArgProgress, false) - } - - err := cmdconfig.ValidateConnectionStringArgs() - if err != nil { - initData.Result.Error = err - return initData - } - - // set color schema - err = initialiseColorScheme() - if err != nil { - initData.Result.Error = err - return initData - } - // load workspace - initData.Workspace, err = loadWorkspacePromptingForVariables(ctx) - if err != nil { - if !utils.IsCancelledError(err) { - err = utils.PrefixError(err, "failed to load workspace") - } - initData.Result.Error = err - return initData - } - - // check if the required plugins are installed - err = initData.Workspace.CheckRequiredPluginsInstalled() - if err != nil { - initData.Result.Error = err - return initData - } - - if len(initData.Workspace.Controls) == 0 { - initData.Result.AddWarnings("no controls found in current workspace") - } - - statushooks.SetStatus(ctx, "Connecting to service...") - // get a client - var client db_common.Client - if connectionString := viper.GetString(constants.ArgConnectionString); connectionString != "" { - client, err = db_client.NewDbClient(ctx, connectionString) - } else { - // when starting the database, installers may trigger their own spinners - client, err = db_local.GetLocalClient(ctx, constants.InvokerCheck) - } - - if err != nil { - initData.Result.Error = err - return initData - } - initData.Client = client - - refreshResult := initData.Client.RefreshConnectionAndSearchPaths(ctx) - if refreshResult.Error != nil { - initData.Result.Error = refreshResult.Error - return initData - } - initData.Result.AddWarnings(refreshResult.Warnings...) - - // setup the session data - prepared statements and introspection tables - sessionDataSource := workspace.NewSessionDataSource(initData.Workspace, nil) - - // register EnsureSessionData as a callback on the client. - // if the underlying SQL client has certain errors (for example context expiry) it will reset the session - // so our client object calls this callback to restore the session data - initData.Client.SetEnsureSessionDataFunc(func(localCtx context.Context, conn *db_common.DatabaseSession) (error, []string) { - return workspace.EnsureSessionData(localCtx, sessionDataSource, conn) - }) + // load the workspace + w, err := loadWorkspacePromptingForVariables(ctx) + utils.FailOnErrorWithMessage(err, "failed to load workspace") + initData := control.NewInitData(ctx, w) return initData } @@ -326,9 +238,6 @@ func handleCheckInitResult(ctx context.Context, initData *control.InitData) bool // if there is are any warnings, exit politely shouldExit := len(initData.Result.Warnings) > 0 - // alternative approach - only stop the control run if there are no controls - //shouldExit := initData.workspace == nil || len(initData.workspace.Controls) == 0 - return shouldExit } @@ -351,24 +260,6 @@ func shouldPrintTiming() bool { (outputFormat == constants.CheckOutputFormatText || outputFormat == constants.CheckOutputFormatBrief) } -func initialiseColorScheme() error { - theme := viper.GetString(constants.ArgTheme) - if !viper.GetBool(constants.ConfigKeyIsTerminalTTY) { - // enforce plain output for non-terminals - theme = "plain" - } - themeDef, ok := controldisplay.ColorSchemes[theme] - if !ok { - return fmt.Errorf("invalid theme '%s'", theme) - } - scheme, err := controldisplay.NewControlColorScheme(themeDef) - if err != nil { - return err - } - controldisplay.ControlColors = scheme - return nil -} - func exportCheckResult(ctx context.Context, d *control.ExportData) { d.WaitGroup.Add(1) go func() { diff --git a/cmd/dashboard.go b/cmd/dashboard.go index 7b499002..2557b26d 100644 --- a/cmd/dashboard.go +++ b/cmd/dashboard.go @@ -3,6 +3,8 @@ package cmd import ( "context" + "github.com/turbot/steampipe/statushooks" + "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/turbot/go-kit/helpers" @@ -10,9 +12,9 @@ import ( "github.com/turbot/steampipe/cmdconfig" "github.com/turbot/steampipe/constants" "github.com/turbot/steampipe/contexthelpers" + "github.com/turbot/steampipe/dashboard" "github.com/turbot/steampipe/dashboard/dashboardassets" "github.com/turbot/steampipe/dashboard/dashboardserver" - "github.com/turbot/steampipe/db/db_local" "github.com/turbot/steampipe/utils" ) @@ -31,19 +33,24 @@ The current mod is the working directory, or the directory specified by the --wo cmdconfig.OnCmd(cmd). AddBoolFlag(constants.ArgHelp, "h", false, "Help for dashboard"). AddStringFlag(constants.ArgDashboardServerListen, "", string(dashboardserver.ListenTypeLocal), "Accept connections from: local (localhost only) or network (open)"). - AddIntFlag(constants.ArgDashboardServerPort, "", constants.DashboardServerDefaultPort, "Dashboard server port.") + AddIntFlag(constants.ArgDashboardServerPort, "", constants.DashboardServerDefaultPort, "Dashboard server port."). + AddBoolFlag(constants.ArgModInstall, "", true, "Specify whether to install mod dependencies before running the dashboard") return cmd } func runDashboardCmd(cmd *cobra.Command, args []string) { + // create context for the dashboard execution ctx, cancel := context.WithCancel(cmd.Context()) + // disable all status messages + dashboardCtx := statushooks.DisableStatusHooks(ctx) + contexthelpers.StartCancelHandler(cancel) logging.LogTime("runDashboardCmd start") defer func() { logging.LogTime("runDashboardCmd end") if r := recover(); r != nil { - utils.ShowError(ctx, helpers.ToError(r)) + utils.ShowError(dashboardCtx, helpers.ToError(r)) } }() @@ -54,16 +61,18 @@ func runDashboardCmd(cmd *cobra.Command, args []string) { utils.FailOnError(serverListen.IsValid()) // ensure dashboard assets are present and extract if not - err := dashboardassets.Ensure(ctx) + err := dashboardassets.Ensure(dashboardCtx) utils.FailOnError(err) - dbClient, err := db_local.GetLocalClient(ctx, constants.InvokerDashboard) - utils.FailOnError(err) + // load the workspace + w, err := loadWorkspacePromptingForVariables(dashboardCtx) + utils.FailOnErrorWithMessage(err, "failed to load workspace") - refreshResult := dbClient.RefreshConnectionAndSearchPaths(ctx) - refreshResult.ShowWarnings() - - server, err := dashboardserver.NewServer(ctx, dbClient) + initData := dashboard.NewInitData(dashboardCtx, w) + if shouldExit := handleDashboardInitResult(dashboardCtx, initData); shouldExit { + return + } + server, err := dashboardserver.NewServer(dashboardCtx, initData.Client, initData.Workspace) if err != nil { utils.FailOnError(err) } @@ -71,7 +80,25 @@ func runDashboardCmd(cmd *cobra.Command, args []string) { server.Start() // wait for the given context to cancel - <-ctx.Done() + <-dashboardCtx.Done() - server.Shutdown(ctx) + server.Shutdown(dashboardCtx) +} + +func handleDashboardInitResult(ctx context.Context, initData *dashboard.InitData) bool { + // if there is an error or cancellation we bomb out + // check for the various kinds of failures + utils.FailOnError(initData.Result.Error) + // cancelled? + if ctx != nil { + utils.FailOnError(ctx.Err()) + } + + // if there is a usage warning we display it + initData.Result.DisplayMessages() + + // if there is are any warnings, exit politely + shouldExit := len(initData.Result.Warnings) > 0 + + return shouldExit } diff --git a/constants/args.go b/constants/args.go index da15ade9..e067cb8f 100644 --- a/constants/args.go +++ b/constants/args.go @@ -2,45 +2,45 @@ package constants // Argument name constants const ( - ArgHelp = "help" - ArgVersion = "version" - ArgForce = "force" - ArgAll = "all" - ArgTimer = "timing" - ArgOn = "on" - ArgOff = "off" - ArgClear = "clear" - ArgDatabasePort = "database-port" - ArgListenAddress = "database-listen" + ArgHelp = "help" + ArgVersion = "version" + ArgForce = "force" + ArgAll = "all" + ArgTimer = "timing" + ArgOn = "on" + ArgOff = "off" + ArgClear = "clear" + ArgDatabasePort = "database-port" + ArgListenAddress = "database-listen" ArgServicePassword = "database-password" ArgDashboardServerListen = "dashboardserver-listen" ArgDashboardServerPort = "dashboardserver-port" - ArgForeground = "foreground" - ArgInvoker = "invoker" - ArgUpdateCheck = "update-check" - ArgInstallDir = "install-dir" - ArgWorkspace = "workspace" - ArgWorkspaceChDir = "workspace-chdir" - ArgWorkspaceDatabase = "workspace-database" - ArgSchemaComments = "schema-comments" - ArgCloudHost = "cloud-host" - ArgCloudToken = "cloud-token" - ArgSearchPath = "search-path" - ArgSearchPathPrefix = "search-path-prefix" - ArgWatch = "watch" - ArgTheme = "theme" - ArgProgress = "progress" - ArgExport = "export" - ArgMaxParallel = "max-parallel" - ArgDryRun = "dry-run" - ArgWhere = "where" - ArgTag = "tag" - ArgVariable = "var" - ArgVarFile = "var-file" - ArgConnectionString = "connection-string" - ArgCheckDisplayWidth = "check-display-width" - ArgPrune = "prune" - ArgModInstall = "mod-install" + ArgForeground = "foreground" + ArgInvoker = "invoker" + ArgUpdateCheck = "update-check" + ArgInstallDir = "install-dir" + ArgWorkspace = "workspace" + ArgWorkspaceChDir = "workspace-chdir" + ArgWorkspaceDatabase = "workspace-database" + ArgSchemaComments = "schema-comments" + ArgCloudHost = "cloud-host" + ArgCloudToken = "cloud-token" + ArgSearchPath = "search-path" + ArgSearchPathPrefix = "search-path-prefix" + ArgWatch = "watch" + ArgTheme = "theme" + ArgProgress = "progress" + ArgExport = "export" + ArgMaxParallel = "max-parallel" + ArgDryRun = "dry-run" + ArgWhere = "where" + ArgTag = "tag" + ArgVariable = "var" + ArgVarFile = "var-file" + ArgConnectionString = "connection-string" + ArgCheckDisplayWidth = "check-display-width" + ArgPrune = "prune" + ArgModInstall = "mod-install" ) /// metaquery mode arguments diff --git a/constants/prepared_statements.go b/constants/prepared_statements.go new file mode 100644 index 00000000..b83cfe45 --- /dev/null +++ b/constants/prepared_statements.go @@ -0,0 +1,11 @@ +package constants + +const MaxPreparedStatementNameLength = 63 +const PreparedStatementQuerySuffix = "_q" +const PreparedStatementControlSuffix = "_c" +const PreparedStatementChartSuffix = "_ch" +const PreparedStatementHierarchySuffix = "_h" +const PreparedStatementImageSuffix = "_im" +const PreparedStatementInputSuffix = "_ip" +const PreparedStatementCardSuffix = "_cd" +const PreparedStatementTableSuffix = "_t" diff --git a/control/controlexecute/control_run.go b/control/controlexecute/control_run.go index 13cb2755..cd8a1f68 100644 --- a/control/controlexecute/control_run.go +++ b/control/controlexecute/control_run.go @@ -303,7 +303,7 @@ func (r *ControlRun) getControlQueryContext(ctx context.Context) context.Context } func (r *ControlRun) resolveControlQuery(control *modconfig.Control) (string, error) { - query, err := r.Tree.workspace.ResolveControlQuery(control, nil) + query, err := r.Tree.workspace.ResolveQuery(control, nil) if err != nil { return "", fmt.Errorf(`cannot run %s - failed to resolve query "%s": %s`, control.Name(), typehelpers.SafeString(control.SQL), err.Error()) } diff --git a/control/init_data.go b/control/init_data.go index e2e2b4b7..fbdb3989 100644 --- a/control/init_data.go +++ b/control/init_data.go @@ -1,7 +1,18 @@ package control import ( + "context" + "fmt" + + "github.com/spf13/viper" + "github.com/turbot/steampipe/cmdconfig" + "github.com/turbot/steampipe/constants" + "github.com/turbot/steampipe/control/controldisplay" + "github.com/turbot/steampipe/db/db_client" "github.com/turbot/steampipe/db/db_common" + "github.com/turbot/steampipe/db/db_local" + "github.com/turbot/steampipe/modinstaller" + "github.com/turbot/steampipe/statushooks" "github.com/turbot/steampipe/workspace" ) @@ -10,3 +21,107 @@ type InitData struct { Client db_common.Client Result *db_common.InitResult } + +func NewInitData(ctx context.Context, w *workspace.Workspace) *InitData { + initData := &InitData{ + Workspace: w, + Result: &db_common.InitResult{}, + } + + if err := controldisplay.EnsureTemplates(); err != nil { + initData.Result.Error = err + return initData + } + + if viper.GetBool(constants.ArgModInstall) { + opts := &modinstaller.InstallOpts{WorkspacePath: viper.GetString(constants.ArgWorkspaceChDir)} + _, err := modinstaller.InstallWorkspaceDependencies(opts) + if err != nil { + initData.Result.Error = err + return initData + } + } + + if viper.GetString(constants.ArgOutput) == constants.CheckOutputFormatNone { + // set progress to false + viper.Set(constants.ArgProgress, false) + } + + err := cmdconfig.ValidateConnectionStringArgs() + if err != nil { + initData.Result.Error = err + return initData + } + + // set color schema + err = initialiseColorScheme() + if err != nil { + initData.Result.Error = err + return initData + } + + // check if the required plugins are installed + err = initData.Workspace.CheckRequiredPluginsInstalled() + if err != nil { + initData.Result.Error = err + return initData + } + + if len(initData.Workspace.GetResourceMaps().Controls) == 0 { + initData.Result.AddWarnings("no controls found in current workspace") + } + + statushooks.SetStatus(ctx, "Connecting to service...") + // get a client + var client db_common.Client + if connectionString := viper.GetString(constants.ArgConnectionString); connectionString != "" { + client, err = db_client.NewDbClient(ctx, connectionString) + } else { + // when starting the database, installers may trigger their own spinners + client, err = db_local.GetLocalClient(ctx, constants.InvokerCheck) + } + + if err != nil { + initData.Result.Error = err + return initData + } + initData.Client = client + + refreshResult := initData.Client.RefreshConnectionAndSearchPaths(ctx) + if refreshResult.Error != nil { + initData.Result.Error = refreshResult.Error + return initData + } + initData.Result.AddWarnings(refreshResult.Warnings...) + + // setup the session data - prepared statements and introspection tables + sessionDataSource := workspace.NewSessionDataSource(initData.Workspace, nil) + + // register EnsureSessionData as a callback on the client. + // if the underlying SQL client has certain errors (for example context expiry) it will reset the session + // so our client object calls this callback to restore the session data + initData.Client.SetEnsureSessionDataFunc(func(localCtx context.Context, conn *db_common.DatabaseSession) (error, []string) { + return workspace.EnsureSessionData(localCtx, sessionDataSource, conn) + }) + + return initData + +} + +func initialiseColorScheme() error { + theme := viper.GetString(constants.ArgTheme) + if !viper.GetBool(constants.ConfigKeyIsTerminalTTY) { + // enforce plain output for non-terminals + theme = "plain" + } + themeDef, ok := controldisplay.ColorSchemes[theme] + if !ok { + return fmt.Errorf("invalid theme '%s'", theme) + } + scheme, err := controldisplay.NewControlColorScheme(themeDef) + if err != nil { + return err + } + controldisplay.ControlColors = scheme + return nil +} diff --git a/dashboard/dashboardassets/ensure.go b/dashboard/dashboardassets/ensure.go index 3b9f2840..a1d6cbda 100644 --- a/dashboard/dashboardassets/ensure.go +++ b/dashboard/dashboardassets/ensure.go @@ -15,8 +15,8 @@ import ( ) func Ensure(ctx context.Context) error { - logging.LogTime("reportassets.Ensure start") - defer logging.LogTime("reportassets.Ensure end") + logging.LogTime("dashboardassets.Ensure start") + defer logging.LogTime("dashboardassets.Ensure end") // load report assets versions.json versionFile, err := loadReportAssetVersionFile() @@ -28,7 +28,7 @@ func Ensure(ctx context.Context) error { return nil } - statushooks.SetStatus(ctx, "Installing reporting server...") + statushooks.SetStatus(ctx, "Installing dashboard server...") defer statushooks.Done(ctx) reportAssetsPath := filepaths.EnsureDashboardAssetsDir() return ociinstaller.InstallAssets(ctx, reportAssetsPath) @@ -47,7 +47,7 @@ func loadReportAssetVersionFile() (*ReportAssetsVersionFile, error) { file, _ := os.ReadFile(versionFilePath) var versionFile ReportAssetsVersionFile if err := json.Unmarshal(file, &versionFile); err != nil { - log.Println("[ERROR]", "Error while reading report assets version file", err) + log.Println("[ERROR]", "Error while reading dashboard assets version file", err) return nil, err } diff --git a/dashboard/dashboardexecute/check_run.go b/dashboard/dashboardexecute/check_run.go index bfb4e3f4..55b1db84 100644 --- a/dashboard/dashboardexecute/check_run.go +++ b/dashboard/dashboardexecute/check_run.go @@ -20,9 +20,9 @@ type CheckRun struct { Error error `json:"error,omitempty"` NodeType string `json:"node_type"` ControlExecutionTree *controlexecute.ExecutionTree `json:"execution_tree"` - DashboardName string `json:"dashboard"` - DashboardNode modconfig.DashboardLeafNode `json:"-"` - Path []string `json:"-"` + DashboardName string `json:"dashboard"` + DashboardNode modconfig.DashboardLeafNode `json:"-"` + Path []string `json:"-"` parent dashboardinterfaces.DashboardNodeParent runStatus dashboardinterfaces.DashboardRunStatus executionTree *DashboardExecutionTree diff --git a/dashboard/dashboardexecute/container_run.go b/dashboard/dashboardexecute/container_run.go index b480c1a1..b386d97b 100644 --- a/dashboard/dashboardexecute/container_run.go +++ b/dashboard/dashboardexecute/container_run.go @@ -11,10 +11,7 @@ import ( "github.com/turbot/steampipe/steampipeconfig/modconfig" ) -// TODO [reports] split into report and container -// update events // DashboardContainerRun is a struct representing a container run - type DashboardContainerRun struct { Name string `json:"name"` Title string `json:"title,omitempty"` diff --git a/dashboard/dashboardexecute/dashboard_execution_tree.go b/dashboard/dashboardexecute/dashboard_execution_tree.go index 4cf1dae7..5d5a41d7 100644 --- a/dashboard/dashboardexecute/dashboard_execution_tree.go +++ b/dashboard/dashboardexecute/dashboard_execution_tree.go @@ -58,7 +58,7 @@ func (e *DashboardExecutionTree) createRootItem(reportName string) (*DashboardRu if parsedName.ItemType != modconfig.BlockTypeDashboard { return nil, fmt.Errorf("reporting type %s cannot be executed directly - only reports may be executed", parsedName.ItemType) } - dashboard, ok := e.workspace.Dashboards[reportName] + dashboard, ok := e.workspace.GetResourceMaps().Dashboards[reportName] if !ok { return nil, fmt.Errorf("report '%s' does not exist in workspace", reportName) } diff --git a/dashboard/dashboardexecute/dashboard_run.go b/dashboard/dashboardexecute/dashboard_run.go index 4c6af76d..e13dc6b6 100644 --- a/dashboard/dashboardexecute/dashboard_run.go +++ b/dashboard/dashboardexecute/dashboard_run.go @@ -113,7 +113,6 @@ func NewDashboardRun(dashboard *modconfig.Dashboard, parent dashboardinterfaces. // Execute implements DashboardRunNode // execute all children and wait for them to complete func (r *DashboardRun) Execute(ctx context.Context) error { - errChan := make(chan error, len(r.Children)) // execute all children asynchronously for _, child := range r.Children { diff --git a/dashboard/dashboardexecute/execute.go b/dashboard/dashboardexecute/execute.go index 76a38039..b62c498b 100644 --- a/dashboard/dashboardexecute/execute.go +++ b/dashboard/dashboardexecute/execute.go @@ -7,7 +7,6 @@ import ( "github.com/turbot/steampipe/dashboard/dashboardevents" "github.com/turbot/steampipe/dashboard/dashboardinterfaces" "github.com/turbot/steampipe/db/db_common" - "github.com/turbot/steampipe/statushooks" "github.com/turbot/steampipe/workspace" ) @@ -20,9 +19,6 @@ func ExecuteDashboardNode(ctx context.Context, dashboardName string, workspace * return fmt.Errorf("dashboard %s is already running", dashboardName) } - // create context for the dashboard execution - // (for now just disable all status messages - replace with event based? ) - dashboardCtx := statushooks.DisableStatusHooks(ctx) executionTree, err := NewReportExecutionTree(dashboardName, client, workspace) if err != nil { return err @@ -38,13 +34,12 @@ func ExecuteDashboardNode(ctx context.Context, dashboardName string, workspace * workspace.PublishDashboardEvent(&dashboardevents.ExecutionComplete{Dashboard: executionTree.Root}) }() - if err := executionTree.Execute(dashboardCtx); err != nil { + if err := executionTree.Execute(ctx); err != nil { if executionTree.Root.GetRunStatus() != dashboardinterfaces.DashboardRunError { // set error state on the root node executionTree.Root.SetError(err) } } - }() return nil diff --git a/dashboard/dashboardexecute/leaf_run.go b/dashboard/dashboardexecute/leaf_run.go index 682e1aa7..5167849a 100644 --- a/dashboard/dashboardexecute/leaf_run.go +++ b/dashboard/dashboardexecute/leaf_run.go @@ -12,15 +12,15 @@ import ( type LeafRun struct { Name string `json:"name"` - Title string `json:"title,omitempty"` - Width int `json:"width,omitempty"` - SQL string `json:"sql,omitempty"` - Data *LeafData `json:"data,omitempty"` + Title string `json:"title,omitempty"` + Width int `json:"width,omitempty"` + SQL string `json:"sql,omitempty"` + Data *LeafData `json:"data,omitempty"` Error error `json:"error,omitempty"` DashboardNode modconfig.DashboardLeafNode `json:"properties"` - NodeType string `json:"node_type"` - DashboardName string `json:"dashboard"` - Path []string `json:"-"` + NodeType string `json:"node_type"` + DashboardName string `json:"dashboard"` + Path []string `json:"-"` parent dashboardinterfaces.DashboardNodeParent runStatus dashboardinterfaces.DashboardRunStatus executionTree *DashboardExecutionTree @@ -34,7 +34,6 @@ func NewLeafRun(resource modconfig.DashboardLeafNode, parent dashboardinterfaces Name: name, Title: resource.GetTitle(), Width: resource.GetWidth(), - SQL: resource.GetSQL(), Path: resource.GetPaths()[0], DashboardNode: resource, DashboardName: executionTree.dashboardName, @@ -52,7 +51,12 @@ func NewLeafRun(resource modconfig.DashboardLeafNode, parent dashboardinterfaces } r.NodeType = parsedName.ItemType // if we have sql, set status to ready - if r.SQL != "" { + if queryProvider, ok := resource.(modconfig.QueryProvider); ok { + sql, err := executionTree.workspace.ResolveQuery(queryProvider, nil) + if err != nil { + return nil, err + } + r.SQL = sql r.runStatus = dashboardinterfaces.DashboardRunReady } @@ -68,7 +72,8 @@ func (r *LeafRun) Execute(ctx context.Context) error { return err } - if r.SQL == "" { + // if there is nothing to do, return + if r.runStatus == dashboardinterfaces.DashboardRunComplete { return nil } diff --git a/dashboard/dashboardinterfaces/run_interface.go b/dashboard/dashboardinterfaces/run_interface.go index c99b3970..d7531376 100644 --- a/dashboard/dashboardinterfaces/run_interface.go +++ b/dashboard/dashboardinterfaces/run_interface.go @@ -10,8 +10,8 @@ type DashboardRunStatus string const ( DashboardRunReady DashboardRunStatus = "ready" - DashboardRunComplete = "complete" - DashboardRunError = "error" + DashboardRunComplete = "complete" + DashboardRunError = "error" ) type DashboardNodeRun interface { diff --git a/dashboard/dashboardserver/server.go b/dashboard/dashboardserver/server.go index abb56043..58813391 100644 --- a/dashboard/dashboardserver/server.go +++ b/dashboard/dashboardserver/server.go @@ -8,10 +8,8 @@ import ( "reflect" "sync" - "github.com/spf13/viper" "github.com/turbot/go-kit/helpers" typeHelpers "github.com/turbot/go-kit/types" - "github.com/turbot/steampipe/constants" "github.com/turbot/steampipe/dashboard/dashboardevents" "github.com/turbot/steampipe/dashboard/dashboardexecute" "github.com/turbot/steampipe/dashboard/dashboardinterfaces" @@ -69,12 +67,8 @@ type DashboardClientInfo struct { Dashboard *string } -func NewServer(ctx context.Context, dbClient db_common.Client) (*Server, error) { +func NewServer(ctx context.Context, dbClient db_common.Client, w *workspace.Workspace) (*Server, error) { outputWait(ctx, "Starting Dashboard Server") - loadedWorkspace, err := workspace.Load(ctx, viper.GetString(constants.ArgWorkspaceChDir)) - if err != nil { - return nil, err - } webSocket := melody.New() @@ -88,11 +82,11 @@ func NewServer(ctx context.Context, dbClient db_common.Client) (*Server, error) mutex: mutex, dashboardClients: dashboardClients, webSocket: webSocket, - workspace: loadedWorkspace, + workspace: w, } - loadedWorkspace.RegisterDashboardEventHandler(server.HandleWorkspaceUpdate) - err = loadedWorkspace.SetupWatcher(ctx, dbClient, func(c context.Context, e error) {}) + w.RegisterDashboardEventHandler(server.HandleWorkspaceUpdate) + err := w.SetupWatcher(ctx, dbClient, func(c context.Context, e error) {}) outputMessage(ctx, "Workspace loaded") return server, err @@ -126,27 +120,19 @@ func buildDashboardMetadataPayload(workspaceResources *modconfig.WorkspaceResour return json.Marshal(payload) } -func buildAvailableDashboardsPayload(workspace *modconfig.WorkspaceResourceMaps) ([]byte, error) { +func buildAvailableDashboardsPayload(workspaceResources *modconfig.WorkspaceResourceMaps) ([]byte, error) { dashboardsByMod := make(map[string]map[string]ModAvailableDashboard) - for _, mod := range workspace.Mods { + for _, mod := range workspaceResources.Mods { _, ok := dashboardsByMod[mod.FullName] if !ok { dashboardsByMod[mod.FullName] = make(map[string]ModAvailableDashboard) } for _, dashboard := range mod.Dashboards { - // Only include dashboards that are top-level (its parent is a mod) - parents := dashboard.GetParents() - for _, parent := range parents { - switch parent.(type) { - case *modconfig.Mod: - dashboardsByMod[mod.FullName][dashboard.FullName] = ModAvailableDashboard{ - Title: typeHelpers.SafeString(dashboard.Title), - FullName: dashboard.FullName, - ShortName: dashboard.ShortName, - } - break - default: - continue + if dashboard.IsTopLevel { + dashboardsByMod[mod.FullName][dashboard.FullName] = ModAvailableDashboard{ + Title: typeHelpers.SafeString(dashboard.Title), + FullName: dashboard.FullName, + ShortName: dashboard.ShortName, } } } @@ -179,14 +165,7 @@ func buildLeafNodeCompletePayload(event *dashboardevents.LeafNodeComplete) ([]by Action: "leaf_node_complete", DashboardNode: event.Node, } - //jsonString, _ := json.Marshal(payload) - //return jsonString - jsonString, err := json.MarshalIndent(payload, "", " ") - fmt.Println(err) - a := string(jsonString) - fmt.Println(a) - - return jsonString, err + return json.Marshal(payload) } func buildExecutionStartedPayload(event *dashboardevents.ExecutionStarted) ([]byte, error) { @@ -475,7 +454,6 @@ func (s *Server) Init(ctx context.Context) { var request ClientRequest // if we could not decode message - ignore if err := json.Unmarshal(msg, &request); err == nil { - switch request.Action { case "get_dashboard_metadata": payload, err := buildDashboardMetadataPayload(s.workspace.GetResourceMaps()) diff --git a/dashboard/init_data.go b/dashboard/init_data.go new file mode 100644 index 00000000..bc53e5c7 --- /dev/null +++ b/dashboard/init_data.go @@ -0,0 +1,85 @@ +package dashboard + +import ( + "context" + + "github.com/spf13/viper" + "github.com/turbot/steampipe/cmdconfig" + "github.com/turbot/steampipe/constants" + "github.com/turbot/steampipe/db/db_client" + "github.com/turbot/steampipe/db/db_common" + "github.com/turbot/steampipe/db/db_local" + "github.com/turbot/steampipe/modinstaller" + "github.com/turbot/steampipe/statushooks" + "github.com/turbot/steampipe/workspace" +) + +type InitData struct { + Workspace *workspace.Workspace + Client db_common.Client + Result *db_common.InitResult +} + +func NewInitData(ctx context.Context, w *workspace.Workspace) *InitData { + initData := &InitData{ + Workspace: w, + Result: &db_common.InitResult{}, + } + + if viper.GetBool(constants.ArgModInstall) { + opts := &modinstaller.InstallOpts{WorkspacePath: viper.GetString(constants.ArgWorkspaceChDir)} + _, err := modinstaller.InstallWorkspaceDependencies(opts) + if err != nil { + initData.Result.Error = err + return initData + } + } + err := cmdconfig.ValidateConnectionStringArgs() + if err != nil { + initData.Result.Error = err + return initData + } + + // check if the required plugins are installed + err = initData.Workspace.CheckRequiredPluginsInstalled() + if err != nil { + initData.Result.Error = err + return initData + } + + statushooks.SetStatus(ctx, "Connecting to service...") + // get a client + var client db_common.Client + if connectionString := viper.GetString(constants.ArgConnectionString); connectionString != "" { + client, err = db_client.NewDbClient(ctx, connectionString) + } else { + // when starting the database, installers may trigger their own spinners + client, err = db_local.GetLocalClient(ctx, constants.InvokerDashboard) + } + + if err != nil { + initData.Result.Error = err + return initData + } + initData.Client = client + + refreshResult := initData.Client.RefreshConnectionAndSearchPaths(ctx) + if refreshResult.Error != nil { + initData.Result.Error = refreshResult.Error + return initData + } + initData.Result.AddWarnings(refreshResult.Warnings...) + + // setup the session data - prepared statements and introspection tables + sessionDataSource := workspace.NewSessionDataSource(initData.Workspace, nil) + + // register EnsureSessionData as a callback on the client. + // if the underlying SQL client has certain errors (for example context expiry) it will reset the session + // so our client object calls this callback to restore the session data + initData.Client.SetEnsureSessionDataFunc(func(localCtx context.Context, conn *db_common.DatabaseSession) (error, []string) { + return workspace.EnsureSessionData(localCtx, sessionDataSource, conn) + }) + + return initData + +} diff --git a/db/db_common/introspection_tables.go b/db/db_common/introspection_tables.go index d051e2fb..de4c6ca5 100644 --- a/db/db_common/introspection_tables.go +++ b/db/db_common/introspection_tables.go @@ -50,7 +50,7 @@ func getCreateTablesSql(commonColumnSql []string) string { createSql = append(createSql, getTableCreateSqlForResource(modconfig.Benchmark{}, constants.IntrospectionTableBenchmark, commonColumnSql)) createSql = append(createSql, getTableCreateSqlForResource(modconfig.Mod{}, constants.IntrospectionTableMod, commonColumnSql)) createSql = append(createSql, getTableCreateSqlForResource(modconfig.Variable{}, constants.IntrospectionTableVariable, commonColumnSql)) - createSql = append(createSql, getTableCreateSqlForResource(modconfig.DashboardContainer{}, constants.IntrospectionTableDashboard, commonColumnSql)) + createSql = append(createSql, getTableCreateSqlForResource(modconfig.Dashboard{}, constants.IntrospectionTableDashboard, commonColumnSql)) createSql = append(createSql, getTableCreateSqlForResource(modconfig.DashboardContainer{}, constants.IntrospectionTableDashboardContainer, commonColumnSql)) createSql = append(createSql, getTableCreateSqlForResource(modconfig.DashboardCard{}, constants.IntrospectionTableDashboardCard, commonColumnSql)) createSql = append(createSql, getTableCreateSqlForResource(modconfig.DashboardChart{}, constants.IntrospectionTableDashboardChart, commonColumnSql)) diff --git a/db/db_common/prepared_statements.go b/db/db_common/prepared_statements.go index 17222f8c..895c2f6d 100644 --- a/db/db_common/prepared_statements.go +++ b/db/db_common/prepared_statements.go @@ -36,38 +36,30 @@ func CreatePreparedStatements(ctx context.Context, resourceMaps *modconfig.Works func GetPreparedStatementsSQL(resourceMaps *modconfig.WorkspaceResourceMaps) map[string]string { // make map of resource name to create SQL sqlMap := make(map[string]string) - for _, query := range resourceMaps.Queries { - // if the query does not have parameters, it is NOT a prepared statement - if len(query.Params) == 0 { - continue + for _, queryProvider := range resourceMaps.QueryProviders() { + if createSQL := getPreparedStatementCreateSql(queryProvider); createSQL != nil { + sqlMap[queryProvider.Name()] = *createSQL } - - // remove trailing semicolons from sql as this breaks the prepare statement - rawSql := cleanPreparedStatementSQL(typehelpers.SafeString(query.SQL)) - preparedStatementName := query.GetPreparedStatementName() - sqlMap[query.FullName] = fmt.Sprintf("PREPARE %s AS (\n%s\n)", preparedStatementName, rawSql) } - - for _, control := range resourceMaps.Controls { - // only create prepared statements for controls with inline SQL - if control.SQL == nil { - continue - } - // if the control does not have parameters, it is NOT a prepared statement - if len(control.Params) == 0 { - continue - } - - // remove trailing semicolons from sql as this breaks the prepare statement - rawSql := strings.TrimRight(strings.TrimSpace(typehelpers.SafeString(control.SQL)), ";") - preparedStatementName := control.GetPreparedStatementName() - sqlMap[control.FullName] = fmt.Sprintf("PREPARE %s AS (\n%s\n)", preparedStatementName, rawSql) - } - return sqlMap } -func cleanPreparedStatementSQL(query string) string { +func getPreparedStatementCreateSql(queryProvider modconfig.QueryProvider) *string { + // if the query does not have parameters, it is NOT a prepared statement + if len(queryProvider.GetParams()) == 0 { + return nil + } + + // if the query provider has params, is MUST define SQL + + // remove trailing semicolons from sql as this breaks the prepare statement + rawSql := cleanPreparedStatementCreateSQL(typehelpers.SafeString(queryProvider.GetSQL())) + preparedStatementName := queryProvider.GetPreparedStatementName() + createSQL := fmt.Sprintf("PREPARE %s AS (\n%s\n)", preparedStatementName, rawSql) + return &createSQL +} + +func cleanPreparedStatementCreateSQL(query string) string { rawSql := strings.TrimRight(strings.TrimSpace(query), ";") return rawSql } diff --git a/steampipeconfig/modconfig/benchmark.go b/steampipeconfig/modconfig/benchmark.go index 5f30e870..b7dee70c 100644 --- a/steampipeconfig/modconfig/benchmark.go +++ b/steampipeconfig/modconfig/benchmark.go @@ -213,11 +213,6 @@ func (b *Benchmark) Name() string { return b.FullName } -// GetSQL implements DashboardLeafNode -func (b *Benchmark) GetSQL() string { - return "" -} - // GetWidth implements DashboardLeafNode func (b *Benchmark) GetWidth() int { if b.Width == nil { diff --git a/steampipeconfig/modconfig/block_type.go b/steampipeconfig/modconfig/block_type.go index 9cfe740f..e570c885 100644 --- a/steampipeconfig/modconfig/block_type.go +++ b/steampipeconfig/modconfig/block_type.go @@ -5,7 +5,7 @@ const ( BlockTypeQuery = "query" BlockTypeControl = "control" BlockTypeBenchmark = "benchmark" - BlockTypeDashboard = "dashboard" + BlockTypeDashboard = "dashboard" BlockTypeContainer = "container" BlockTypeChart = "chart" BlockTypeCard = "card" diff --git a/steampipeconfig/modconfig/control.go b/steampipeconfig/modconfig/control.go index c5faad48..3a5a5701 100644 --- a/steampipeconfig/modconfig/control.go +++ b/steampipeconfig/modconfig/control.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/turbot/go-kit/types" typehelpers "github.com/turbot/go-kit/types" + "github.com/turbot/steampipe/constants" "github.com/turbot/steampipe/utils" "github.com/zclconf/go-cty/cty" ) @@ -15,6 +16,7 @@ import ( type Control struct { DashboardLeafNodeBase ResourceWithMetadataBase + QueryProviderBase // required to allow partial decoding Remain hcl.Body `hcl:",remain" json:"-"` @@ -52,15 +54,15 @@ type Control struct { func NewControl(block *hcl.Block, mod *Mod) *Control { shortName := GetAnonymousResourceShortName(block, mod) control := &Control{ - ShortName: block.Labels[0], + ShortName: shortName, FullName: fmt.Sprintf("%s.control.%s", mod.ShortName, shortName), UnqualifiedName: fmt.Sprintf("control.%s", shortName), Mod: mod, DeclRange: block.DefRange, Args: NewQueryArgs(), } - control.SetAnonymous(block) + control.SetAnonymous(block) return control } @@ -269,21 +271,6 @@ func (c *Control) GetDeclRange() *hcl.Range { return &c.DeclRange } -// GetMetadata implements ResourceWithMetadata -func (c *Control) GetMetadata() *ResourceMetadata { - return c.metadata -} - -// SetMetadata implements ResourceWithMetadata -func (c *Control) SetMetadata(metadata *ResourceMetadata) { - c.metadata = metadata -} - -// GetModName implements QueryProvider -func (c *Control) GetModName() string { - return c.Mod.NameWithVersion() -} - // GetParams implements QueryProvider func (c *Control) GetParams() []*ParamDef { return c.Params @@ -294,23 +281,14 @@ func (c *Control) GetQuery() *Query { return c.Query } -// GetPreparedStatementName implements QueryProvider -func (c *Control) GetPreparedStatementName() string { - // lazy load - if c.PreparedStatementName == "" { - c.PreparedStatementName = preparedStatementName(c) - } - return c.PreparedStatementName -} - // GetArgs implements QueryProvider func (c *Control) GetArgs() *QueryArgs { return c.Args } -// GetSQL implements QueryProvider, DashboardLeafNode -func (c *Control) GetSQL() string { - return typehelpers.SafeString(c.SQL) +// GetSQL implements QueryProvider +func (c *Control) GetSQL() *string { + return c.SQL } // SetArgs implements QueryProvider @@ -323,6 +301,21 @@ func (c *Control) SetParams(params []*ParamDef) { c.Params = params } +// GetPreparedStatementName implements QueryProvider +func (c *Control) GetPreparedStatementName() string { + if c.PreparedStatementName != "" { + return c.PreparedStatementName + } + c.PreparedStatementName = c.buildPreparedStatementName(c.ShortName, c.Mod.NameWithVersion(), constants.PreparedStatementControlSuffix) + return c.PreparedStatementName +} + +// GetPreparedStatementExecuteSQL implements QueryProvider +func (c *Control) GetPreparedStatementExecuteSQL(args *QueryArgs) (string, error) { + // defer to base + return c.getPreparedStatementExecuteSQL(c, args) +} + // GetWidth implements DashboardLeafNode func (c *Control) GetWidth() int { if c.Width == nil { diff --git a/steampipeconfig/modconfig/dashboard.go b/steampipeconfig/modconfig/dashboard.go index 9dcb923e..cb5ca772 100644 --- a/steampipeconfig/modconfig/dashboard.go +++ b/steampipeconfig/modconfig/dashboard.go @@ -29,20 +29,21 @@ type Dashboard struct { Width *int `cty:"width" hcl:"width" column:"width,text"` Args *QueryArgs `cty:"args" column:"args,jsonb"` Base *Dashboard `hcl:"base"` - Inputs []*DashboardInput `cty:"inputs"` - Display *string `cty:"display" hcl:"display" json:"display,omitempty"` + Inputs []*DashboardInput `cty:"inputs" column:"inputs,jsonb"` + Display *string `cty:"display" hcl:"display" column:"display,text" json:"display,omitempty"` OnHooks []*DashboardOn `cty:"on" hcl:"on,block" json:"on,omitempty"` - Mod *Mod `cty:"mod"` - DeclRange hcl.Range - Paths []NodePath `column:"path,jsonb"` + IsTopLevel bool `column:"is_top_level,bool"` + Mod *Mod `cty:"mod"` + DeclRange hcl.Range + Paths []NodePath `column:"path,jsonb"` // store children in a way which can be serialised via cty ChildNames []string `cty:"children" column:"children,jsonb"` selfInputsMap map[string]*DashboardInput // the actual children children []ModTreeItem - // TODO [reports] can a dashboard ever have multiple parents + // TODO [reports] can a dashboard ever have multiple parents?? parents []ModTreeItem runtimeDependencyGraph *topsort.Graph @@ -125,6 +126,11 @@ func (d *Dashboard) GetDeclRange() *hcl.Range { func (d *Dashboard) AddParent(parent ModTreeItem) error { d.parents = append(d.parents, parent) + // if our parent is a mod, we are a top level dashboard + if _, ok := parent.(*Mod); ok { + d.IsTopLevel = true + } + return nil } diff --git a/steampipeconfig/modconfig/dashboard_card.go b/steampipeconfig/modconfig/dashboard_card.go index c361b4e3..952caf5c 100644 --- a/steampipeconfig/modconfig/dashboard_card.go +++ b/steampipeconfig/modconfig/dashboard_card.go @@ -3,6 +3,8 @@ package modconfig import ( "fmt" + "github.com/turbot/steampipe/constants" + "github.com/turbot/steampipe/utils" "github.com/hashicorp/hcl/v2" @@ -14,6 +16,7 @@ import ( type DashboardCard struct { DashboardLeafNodeBase ResourceWithMetadataBase + QueryProviderBase FullName string `cty:"name" json:"-"` ShortName string `json:"-"` @@ -53,10 +56,9 @@ func NewDashboardCard(block *hcl.Block, mod *Mod) *DashboardCard { Mod: mod, DeclRange: block.DefRange, } + c.SetAnonymous(block) - return c - } func (c *DashboardCard) Equals(other *DashboardCard) bool { @@ -165,16 +167,6 @@ func (c *DashboardCard) SetPaths() { } } -// GetMetadata implements ResourceWithMetadata -func (c *DashboardCard) GetMetadata() *ResourceMetadata { - return c.metadata -} - -// SetMetadata implements ResourceWithMetadata -func (c *DashboardCard) SetMetadata(metadata *ResourceMetadata) { - c.metadata = metadata -} - func (c *DashboardCard) Diff(other *DashboardCard) *DashboardTreeItemDiffs { res := &DashboardTreeItemDiffs{ Item: c, @@ -232,9 +224,9 @@ func (c *DashboardCard) GetArgs() *QueryArgs { return c.Args } -// GetSQL implements QueryProvider, DashboardLeafNode -func (c *DashboardCard) GetSQL() string { - return typehelpers.SafeString(c.SQL) +// GetSQL implements QueryProvider +func (c *DashboardCard) GetSQL() *string { + return c.SQL } // GetQuery implements QueryProvider @@ -242,26 +234,27 @@ func (c *DashboardCard) GetQuery() *Query { return c.Query } -// GetPreparedStatementName implements QueryProvider -func (c *DashboardCard) GetPreparedStatementName() string { - // lazy load - if c.PreparedStatementName == "" { - c.PreparedStatementName = preparedStatementName(c) - } - return c.PreparedStatementName -} - -// GetModName implements QueryProvider -func (c *DashboardCard) GetModName() string { - return c.Mod.NameWithVersion() -} - // SetArgs implements QueryProvider func (c *DashboardCard) SetArgs(args *QueryArgs) { - // nothing + c.Args = args } // SetParams implements QueryProvider func (c *DashboardCard) SetParams(params []*ParamDef) { c.Params = params } + +// GetPreparedStatementName implements QueryProvider +func (c *DashboardCard) GetPreparedStatementName() string { + if c.PreparedStatementName != "" { + return c.PreparedStatementName + } + c.PreparedStatementName = c.buildPreparedStatementName(c.ShortName, c.Mod.NameWithVersion(), constants.PreparedStatementCardSuffix) + return c.PreparedStatementName +} + +// GetPreparedStatementExecuteSQL implements QueryProvider +func (c *DashboardCard) GetPreparedStatementExecuteSQL(args *QueryArgs) (string, error) { + // defer to base + return c.getPreparedStatementExecuteSQL(c, args) +} diff --git a/steampipeconfig/modconfig/dashboard_chart.go b/steampipeconfig/modconfig/dashboard_chart.go index f18d0248..eeec890d 100644 --- a/steampipeconfig/modconfig/dashboard_chart.go +++ b/steampipeconfig/modconfig/dashboard_chart.go @@ -3,6 +3,8 @@ package modconfig import ( "fmt" + "github.com/turbot/steampipe/constants" + "github.com/hashicorp/hcl/v2" typehelpers "github.com/turbot/go-kit/types" "github.com/turbot/steampipe/utils" @@ -13,6 +15,7 @@ import ( type DashboardChart struct { DashboardLeafNodeBase ResourceWithMetadataBase + QueryProviderBase // required to allow partial decoding Remain hcl.Body `hcl:",remain" json:"-"` @@ -59,8 +62,8 @@ func NewDashboardChart(block *hcl.Block, mod *Mod) *DashboardChart { Mod: mod, DeclRange: block.DefRange, } - c.SetAnonymous(block) + c.SetAnonymous(block) return c } @@ -284,9 +287,9 @@ func (c *DashboardChart) GetArgs() *QueryArgs { return c.Args } -// GetSQL implements QueryProvider, DashboardLeafNode -func (c *DashboardChart) GetSQL() string { - return typehelpers.SafeString(c.SQL) +// GetSQL implements QueryProvider +func (c *DashboardChart) GetSQL() *string { + return c.SQL } // GetQuery implements QueryProvider @@ -294,20 +297,6 @@ func (c *DashboardChart) GetQuery() *Query { return c.Query } -// GetPreparedStatementName implements QueryProvider -func (c *DashboardChart) GetPreparedStatementName() string { - // lazy load - if c.PreparedStatementName == "" { - c.PreparedStatementName = preparedStatementName(c) - } - return c.PreparedStatementName -} - -// GetModName implements QueryProvider -func (c *DashboardChart) GetModName() string { - return c.Mod.NameWithVersion() -} - // SetArgs implements QueryProvider func (c *DashboardChart) SetArgs(args *QueryArgs) { // nothing @@ -317,3 +306,18 @@ func (c *DashboardChart) SetArgs(args *QueryArgs) { func (c *DashboardChart) SetParams(params []*ParamDef) { c.Params = params } + +// GetPreparedStatementName implements QueryProvider +func (c *DashboardChart) GetPreparedStatementName() string { + if c.PreparedStatementName != "" { + return c.PreparedStatementName + } + c.PreparedStatementName = c.buildPreparedStatementName(c.ShortName, c.Mod.NameWithVersion(), constants.PreparedStatementChartSuffix) + return c.PreparedStatementName +} + +// GetPreparedStatementExecuteSQL implements QueryProvider +func (c *DashboardChart) GetPreparedStatementExecuteSQL(args *QueryArgs) (string, error) { + // defer to base + return c.getPreparedStatementExecuteSQL(c, args) +} diff --git a/steampipeconfig/modconfig/dashboard_chart_axes.go b/steampipeconfig/modconfig/dashboard_chart_axes.go index 0535f5b9..ec5c7775 100644 --- a/steampipeconfig/modconfig/dashboard_chart_axes.go +++ b/steampipeconfig/modconfig/dashboard_chart_axes.go @@ -102,7 +102,7 @@ type DashboardChartAxesY struct { Title *DashboardChartAxisTitle `cty:"title" hcl:"title,block" json:"title,omitempty"` Labels *DashboardChartLabels `cty:"labels" hcl:"labels,block" json:"labels,omitempty"` Min *int `cty:"min" hcl:"min" json:"min,omitempty"` - Max *int `cty:"max" hcl:"max" json:"max,omitempty"` + Max *int `cty:"max" hcl:"max" json:"max,omitempty"` } func (y *DashboardChartAxesY) Equals(other *DashboardChartAxesY) bool { diff --git a/steampipeconfig/modconfig/dashboard_hierarchy.go b/steampipeconfig/modconfig/dashboard_hierarchy.go index 1ec1fe70..38deb692 100644 --- a/steampipeconfig/modconfig/dashboard_hierarchy.go +++ b/steampipeconfig/modconfig/dashboard_hierarchy.go @@ -3,6 +3,8 @@ package modconfig import ( "fmt" + "github.com/turbot/steampipe/constants" + "github.com/hashicorp/hcl/v2" typehelpers "github.com/turbot/go-kit/types" "github.com/turbot/steampipe/utils" @@ -13,6 +15,7 @@ import ( type DashboardHierarchy struct { DashboardLeafNodeBase ResourceWithMetadataBase + QueryProviderBase // required to allow partial decoding Remain hcl.Body `hcl:",remain" json:"-"` @@ -56,7 +59,6 @@ func NewDashboardHierarchy(block *hcl.Block, mod *Mod) *DashboardHierarchy { DeclRange: block.DefRange, } h.SetAnonymous(block) - return h } @@ -241,9 +243,9 @@ func (h *DashboardHierarchy) GetArgs() *QueryArgs { return h.Args } -// GetSQL implements QueryProvider, DashboardLeafNode -func (h *DashboardHierarchy) GetSQL() string { - return typehelpers.SafeString(h.SQL) +// GetSQL implements QueryProvider +func (h *DashboardHierarchy) GetSQL() *string { + return h.SQL } // GetQuery implements QueryProvider @@ -251,20 +253,6 @@ func (h *DashboardHierarchy) GetQuery() *Query { return h.Query } -// GetPreparedStatementName implements QueryProvider -func (h *DashboardHierarchy) GetPreparedStatementName() string { - // lazy load - if h.PreparedStatementName == "" { - h.PreparedStatementName = preparedStatementName(h) - } - return h.PreparedStatementName -} - -// GetModName implements QueryProvider -func (h *DashboardHierarchy) GetModName() string { - return h.Mod.NameWithVersion() -} - // SetArgs implements QueryProvider func (h *DashboardHierarchy) SetArgs(args *QueryArgs) { // nothing @@ -274,3 +262,18 @@ func (h *DashboardHierarchy) SetArgs(args *QueryArgs) { func (h *DashboardHierarchy) SetParams(params []*ParamDef) { h.Params = params } + +// GetPreparedStatementName implements QueryProvider +func (h *DashboardHierarchy) GetPreparedStatementName() string { + if h.PreparedStatementName != "" { + return h.PreparedStatementName + } + h.PreparedStatementName = h.buildPreparedStatementName(h.ShortName, h.Mod.NameWithVersion(), constants.PreparedStatementHierarchySuffix) + return h.PreparedStatementName +} + +// GetPreparedStatementExecuteSQL implements QueryProvider +func (h *DashboardHierarchy) GetPreparedStatementExecuteSQL(args *QueryArgs) (string, error) { + // defer to base + return h.getPreparedStatementExecuteSQL(h, args) +} diff --git a/steampipeconfig/modconfig/dashboard_image.go b/steampipeconfig/modconfig/dashboard_image.go index bd4bd7cd..7dce4d34 100644 --- a/steampipeconfig/modconfig/dashboard_image.go +++ b/steampipeconfig/modconfig/dashboard_image.go @@ -3,6 +3,8 @@ package modconfig import ( "fmt" + "github.com/turbot/steampipe/constants" + "github.com/hashicorp/hcl/v2" typehelpers "github.com/turbot/go-kit/types" "github.com/turbot/steampipe/utils" @@ -13,20 +15,28 @@ import ( type DashboardImage struct { DashboardLeafNodeBase ResourceWithMetadataBase + QueryProviderBase FullName string `cty:"name" json:"-"` ShortName string `json:"-"` UnqualifiedName string `json:"-"` // these properties are JSON serialised by the parent LeafRun - Title *string `cty:"title" hcl:"title" column:"title,text" json:"-"` - Width *int `cty:"width" hcl:"width" column:"width,text" json:"-"` - SQL *string `cty:"sql" hcl:"sql" column:"sql,text" json:"-"` - Src *string `cty:"src" hcl:"src" column:"src,text" json:"src,omitempty"` - Alt *string `cty:"alt" hcl:"alt" column:"alt,text" json:"alt,omitempty"` - Display *string `cty:"display" hcl:"display" json:"display,omitempty"` - OnHooks []*DashboardOn `cty:"on" hcl:"on,block" json:"on,omitempty"` - Base *DashboardImage `hcl:"base" json:"-"` + Title *string `cty:"title" hcl:"title" column:"title,text" json:"-"` + Width *int `cty:"width" hcl:"width" column:"width,text" json:"-"` + Src *string `cty:"src" hcl:"src" column:"src,text" json:"src,omitempty"` + Alt *string `cty:"alt" hcl:"alt" column:"alt,text" json:"alt,omitempty"` + Display *string `cty:"display" hcl:"display" json:"display,omitempty"` + OnHooks []*DashboardOn `cty:"on" hcl:"on,block" json:"on,omitempty"` + + // QueryProvider + SQL *string `cty:"sql" hcl:"sql" column:"sql,text" json:"sql"` + Query *Query `hcl:"query" json:"-"` + PreparedStatementName string `column:"prepared_statement_name,text" json:"-"` + Args *QueryArgs `cty:"args" column:"args,jsonb" json:"args"` + Params []*ParamDef `cty:"params" column:"params,jsonb" json:"params"` + + Base *DashboardImage `hcl:"base" json:"-"` DeclRange hcl.Range `json:"-"` Mod *Mod `cty:"mod" json:"-"` @@ -48,160 +58,194 @@ func NewDashboardImage(block *hcl.Block, mod *Mod) *DashboardImage { return i } -func (c *DashboardImage) Equals(other *DashboardImage) bool { - diff := c.Diff(other) +func (i *DashboardImage) Equals(other *DashboardImage) bool { + diff := i.Diff(other) return !diff.HasChanges() } // CtyValue implements HclResource -func (c *DashboardImage) CtyValue() (cty.Value, error) { - return getCtyValue(c) +func (i *DashboardImage) CtyValue() (cty.Value, error) { + return getCtyValue(i) } // Name implements HclResource, ModTreeItem // return name in format: 'image.' -func (c *DashboardImage) Name() string { - return c.FullName +func (i *DashboardImage) Name() string { + return i.FullName } // OnDecoded implements HclResource -func (c *DashboardImage) OnDecoded(*hcl.Block) hcl.Diagnostics { - c.setBaseProperties() +func (i *DashboardImage) OnDecoded(*hcl.Block) hcl.Diagnostics { + i.setBaseProperties() return nil } -func (c *DashboardImage) setBaseProperties() { - if c.Base == nil { +func (i *DashboardImage) setBaseProperties() { + if i.Base == nil { return } - if c.Title == nil { - c.Title = c.Base.Title + if i.Title == nil { + i.Title = i.Base.Title } - if c.Src == nil { - c.Src = c.Base.Src + if i.Src == nil { + i.Src = i.Base.Src } - if c.Alt == nil { - c.Alt = c.Base.Alt + if i.Alt == nil { + i.Alt = i.Base.Alt } - if c.Width == nil { - c.Width = c.Base.Width - } - if c.SQL == nil { - c.SQL = c.Base.SQL + if i.Width == nil { + i.Width = i.Base.Width } } // AddReference implements HclResource -func (c *DashboardImage) AddReference(*ResourceReference) {} +func (i *DashboardImage) AddReference(*ResourceReference) {} // GetMod implements HclResource -func (c *DashboardImage) GetMod() *Mod { - return c.Mod +func (i *DashboardImage) GetMod() *Mod { + return i.Mod } // GetDeclRange implements HclResource -func (c *DashboardImage) GetDeclRange() *hcl.Range { - return &c.DeclRange +func (i *DashboardImage) GetDeclRange() *hcl.Range { + return &i.DeclRange } // AddParent implements ModTreeItem -func (c *DashboardImage) AddParent(parent ModTreeItem) error { - c.parents = append(c.parents, parent) +func (i *DashboardImage) AddParent(parent ModTreeItem) error { + i.parents = append(i.parents, parent) return nil } // GetParents implements ModTreeItem -func (c *DashboardImage) GetParents() []ModTreeItem { - return c.parents +func (i *DashboardImage) GetParents() []ModTreeItem { + return i.parents } // GetChildren implements ModTreeItem -func (c *DashboardImage) GetChildren() []ModTreeItem { +func (i *DashboardImage) GetChildren() []ModTreeItem { return nil } // GetTitle implements ModTreeItem -func (c *DashboardImage) GetTitle() string { - return typehelpers.SafeString(c.Title) +func (i *DashboardImage) GetTitle() string { + return typehelpers.SafeString(i.Title) } // GetDescription implements ModTreeItem -func (c *DashboardImage) GetDescription() string { +func (i *DashboardImage) GetDescription() string { return "" } // GetTags implements ModTreeItem -func (c *DashboardImage) GetTags() map[string]string { +func (i *DashboardImage) GetTags() map[string]string { return nil } // GetPaths implements ModTreeItem -func (c *DashboardImage) GetPaths() []NodePath { +func (i *DashboardImage) GetPaths() []NodePath { // lazy load - if len(c.Paths) == 0 { - c.SetPaths() + if len(i.Paths) == 0 { + i.SetPaths() } - return c.Paths + return i.Paths } // SetPaths implements ModTreeItem -func (c *DashboardImage) SetPaths() { - for _, parent := range c.parents { +func (i *DashboardImage) SetPaths() { + for _, parent := range i.parents { for _, parentPath := range parent.GetPaths() { - c.Paths = append(c.Paths, append(parentPath, c.Name())) + i.Paths = append(i.Paths, append(parentPath, i.Name())) } } } -func (c *DashboardImage) Diff(other *DashboardImage) *DashboardTreeItemDiffs { +func (i *DashboardImage) Diff(other *DashboardImage) *DashboardTreeItemDiffs { res := &DashboardTreeItemDiffs{ - Item: c, - Name: c.Name(), + Item: i, + Name: i.Name(), } - if !utils.SafeStringsEqual(c.FullName, other.FullName) { + if !utils.SafeStringsEqual(i.FullName, other.FullName) { res.AddPropertyDiff("Name") } - if !utils.SafeStringsEqual(c.Title, other.Title) { + if !utils.SafeStringsEqual(i.Title, other.Title) { res.AddPropertyDiff("Title") } - if !utils.SafeStringsEqual(c.SQL, other.SQL) { - res.AddPropertyDiff("SQL") - } - - if !utils.SafeIntEqual(c.Width, other.Width) { + if !utils.SafeIntEqual(i.Width, other.Width) { res.AddPropertyDiff("Width") } - if !utils.SafeStringsEqual(c.Src, other.Src) { + if !utils.SafeStringsEqual(i.Src, other.Src) { res.AddPropertyDiff("Src") } - if !utils.SafeStringsEqual(c.Alt, other.Alt) { + if !utils.SafeStringsEqual(i.Alt, other.Alt) { res.AddPropertyDiff("Alt") } - res.populateChildDiffs(c, other) + res.populateChildDiffs(i, other) return res } -// GetSQL implements DashboardLeafNode -func (c *DashboardImage) GetSQL() string { - return typehelpers.SafeString(c.SQL) -} - // GetWidth implements DashboardLeafNode -func (c *DashboardImage) GetWidth() int { - if c.Width == nil { +func (i *DashboardImage) GetWidth() int { + if i.Width == nil { return 0 } - return *c.Width + return *i.Width } // GetUnqualifiedName implements DashboardLeafNode, ModTreeItem -func (c *DashboardImage) GetUnqualifiedName() string { - return c.UnqualifiedName +func (i *DashboardImage) GetUnqualifiedName() string { + return i.UnqualifiedName +} + +// GetParams implements QueryProvider +func (i *DashboardImage) GetParams() []*ParamDef { + return i.Params +} + +// GetArgs implements QueryProvider +func (i *DashboardImage) GetArgs() *QueryArgs { + return i.Args + +} + +// GetSQL implements QueryProvider +func (i *DashboardImage) GetSQL() *string { + return i.SQL +} + +// GetQuery implements QueryProvider +func (i *DashboardImage) GetQuery() *Query { + return i.Query +} + +// SetArgs implements QueryProvider +func (i *DashboardImage) SetArgs(args *QueryArgs) { + i.Args = args +} + +// SetParams implements QueryProvider +func (i *DashboardImage) SetParams(params []*ParamDef) { + i.Params = params +} + +// GetPreparedStatementName implements QueryProvider +func (i *DashboardImage) GetPreparedStatementName() string { + if i.PreparedStatementName != "" { + return i.PreparedStatementName + } + i.PreparedStatementName = i.buildPreparedStatementName(i.ShortName, i.Mod.NameWithVersion(), constants.PreparedStatementImageSuffix) + return i.PreparedStatementName +} + +// GetPreparedStatementExecuteSQL implements QueryProvider +func (i *DashboardImage) GetPreparedStatementExecuteSQL(args *QueryArgs) (string, error) { + // defer to base + return i.getPreparedStatementExecuteSQL(i, args) } diff --git a/steampipeconfig/modconfig/dashboard_input.go b/steampipeconfig/modconfig/dashboard_input.go index 85f67686..35eb3830 100644 --- a/steampipeconfig/modconfig/dashboard_input.go +++ b/steampipeconfig/modconfig/dashboard_input.go @@ -3,6 +3,8 @@ package modconfig import ( "fmt" + "github.com/turbot/steampipe/constants" + "github.com/hashicorp/hcl/v2" typehelpers "github.com/turbot/go-kit/types" "github.com/turbot/steampipe/utils" @@ -13,6 +15,7 @@ import ( type DashboardInput struct { DashboardLeafNodeBase ResourceWithMetadataBase + QueryProviderBase FullName string `cty:"name" json:"-"` ShortName string `json:"-"` @@ -21,13 +24,19 @@ type DashboardInput struct { // these properties are JSON serialised by the parent LeafRun Title *string `cty:"title" hcl:"title" column:"title,text" json:"-"` Width *int `cty:"width" hcl:"width" column:"width,text" json:"-"` - SQL *string `cty:"sql" hcl:"sql" column:"sql,text" json:"sql"` Type *string `cty:"type" hcl:"type" column:"type,text" json:"type,omitempty"` Style *string `cty:"style" hcl:"style" column:"style,text" json:"style,omitempty"` Value *string `json:"value"` Display *string `cty:"display" hcl:"display" json:"display,omitempty"` OnHooks []*DashboardOn `cty:"on" hcl:"on,block" json:"on,omitempty"` + // QueryProvider + SQL *string `cty:"sql" hcl:"sql" column:"sql,text" json:"sql"` + Query *Query `hcl:"query" json:"-"` + PreparedStatementName string `column:"prepared_statement_name,text" json:"-"` + Args *QueryArgs `cty:"args" column:"args,jsonb" json:"args"` + Params []*ParamDef `cty:"params" column:"params,jsonb" json:"params"` + Base *DashboardInput `hcl:"base" json:"-"` DeclRange hcl.Range `json:"-"` @@ -39,6 +48,7 @@ type DashboardInput struct { } func NewDashboardInput(block *hcl.Block, mod *Mod) *DashboardInput { + // input cannot be anonymous shortName := block.Labels[0] i := &DashboardInput{ ShortName: shortName, @@ -50,159 +60,205 @@ func NewDashboardInput(block *hcl.Block, mod *Mod) *DashboardInput { return i } -func (c *DashboardInput) Equals(other *DashboardInput) bool { - diff := c.Diff(other) +func (i *DashboardInput) Equals(other *DashboardInput) bool { + diff := i.Diff(other) return !diff.HasChanges() } // CtyValue implements HclResource -func (c *DashboardInput) CtyValue() (cty.Value, error) { - return getCtyValue(c) +func (i *DashboardInput) CtyValue() (cty.Value, error) { + return getCtyValue(i) } // Name implements HclResource, ModTreeItem // return name in format: 'chart.' -func (c *DashboardInput) Name() string { - return c.FullName +func (i *DashboardInput) Name() string { + return i.FullName } // OnDecoded implements HclResource -func (c *DashboardInput) OnDecoded(*hcl.Block) hcl.Diagnostics { - c.setBaseProperties() +func (i *DashboardInput) OnDecoded(*hcl.Block) hcl.Diagnostics { + i.setBaseProperties() return nil } -func (c *DashboardInput) setBaseProperties() { - if c.Base == nil { +func (i *DashboardInput) setBaseProperties() { + if i.Base == nil { return } - if c.Title == nil { - c.Title = c.Base.Title + if i.Title == nil { + i.Title = i.Base.Title } - if c.Type == nil { - c.Type = c.Base.Type + if i.Type == nil { + i.Type = i.Base.Type } - if c.Width == nil { - c.Width = c.Base.Width + if i.Width == nil { + i.Width = i.Base.Width } - if c.SQL == nil { - c.SQL = c.Base.SQL + if i.SQL == nil { + i.SQL = i.Base.SQL } } // AddReference implements HclResource -func (c *DashboardInput) AddReference(*ResourceReference) {} +func (i *DashboardInput) AddReference(*ResourceReference) {} // GetMod implements HclResource -func (c *DashboardInput) GetMod() *Mod { - return c.Mod +func (i *DashboardInput) GetMod() *Mod { + return i.Mod } // GetDeclRange implements HclResource -func (c *DashboardInput) GetDeclRange() *hcl.Range { - return &c.DeclRange +func (i *DashboardInput) GetDeclRange() *hcl.Range { + return &i.DeclRange } // AddParent implements ModTreeItem -func (c *DashboardInput) AddParent(parent ModTreeItem) error { - c.parents = append(c.parents, parent) +func (i *DashboardInput) AddParent(parent ModTreeItem) error { + i.parents = append(i.parents, parent) return nil } // GetParents implements ModTreeItem -func (c *DashboardInput) GetParents() []ModTreeItem { - return c.parents +func (i *DashboardInput) GetParents() []ModTreeItem { + return i.parents } // GetChildren implements ModTreeItem -func (c *DashboardInput) GetChildren() []ModTreeItem { +func (i *DashboardInput) GetChildren() []ModTreeItem { return nil } // GetTitle implements ModTreeItem -func (c *DashboardInput) GetTitle() string { - return typehelpers.SafeString(c.Title) +func (i *DashboardInput) GetTitle() string { + return typehelpers.SafeString(i.Title) } // GetDescription implements ModTreeItem -func (c *DashboardInput) GetDescription() string { +func (i *DashboardInput) GetDescription() string { return "" } // GetTags implements ModTreeItem -func (c *DashboardInput) GetTags() map[string]string { +func (i *DashboardInput) GetTags() map[string]string { return nil } // GetPaths implements ModTreeItem -func (c *DashboardInput) GetPaths() []NodePath { +func (i *DashboardInput) GetPaths() []NodePath { // lazy load - if len(c.Paths) == 0 { - c.SetPaths() + if len(i.Paths) == 0 { + i.SetPaths() } - return c.Paths + return i.Paths } // SetPaths implements ModTreeItem -func (c *DashboardInput) SetPaths() { - for _, parent := range c.parents { +func (i *DashboardInput) SetPaths() { + for _, parent := range i.parents { for _, parentPath := range parent.GetPaths() { - c.Paths = append(c.Paths, append(parentPath, c.Name())) + i.Paths = append(i.Paths, append(parentPath, i.Name())) } } } -func (c *DashboardInput) Diff(other *DashboardInput) *DashboardTreeItemDiffs { +func (i *DashboardInput) Diff(other *DashboardInput) *DashboardTreeItemDiffs { res := &DashboardTreeItemDiffs{ - Item: c, - Name: c.Name(), + Item: i, + Name: i.Name(), } - if !utils.SafeStringsEqual(c.FullName, other.FullName) { + if !utils.SafeStringsEqual(i.FullName, other.FullName) { res.AddPropertyDiff("Name") } - if !utils.SafeStringsEqual(c.Title, other.Title) { + if !utils.SafeStringsEqual(i.Title, other.Title) { res.AddPropertyDiff("Title") } - if !utils.SafeStringsEqual(c.SQL, other.SQL) { + if !utils.SafeStringsEqual(i.SQL, other.SQL) { res.AddPropertyDiff("SQL") } - if !utils.SafeIntEqual(c.Width, other.Width) { + if !utils.SafeIntEqual(i.Width, other.Width) { res.AddPropertyDiff("Width") } - if !utils.SafeStringsEqual(c.Type, other.Type) { + if !utils.SafeStringsEqual(i.Type, other.Type) { res.AddPropertyDiff("Type") } - res.populateChildDiffs(c, other) + res.populateChildDiffs(i, other) return res } // GetWidth implements DashboardLeafNode -func (c *DashboardInput) GetWidth() int { - if c.Width == nil { +func (i *DashboardInput) GetWidth() int { + if i.Width == nil { return 0 } - return *c.Width + return *i.Width } // GetUnqualifiedName implements DashboardLeafNode, ModTreeItem -func (c *DashboardInput) GetUnqualifiedName() string { - return c.UnqualifiedName +func (i *DashboardInput) GetUnqualifiedName() string { + return i.UnqualifiedName } // SetDashboard sets the parent dashboard container -func (c *DashboardInput) SetDashboard(dashboard *Dashboard) { - c.dashboard = dashboard +func (i *DashboardInput) SetDashboard(dashboard *Dashboard) { + i.dashboard = dashboard // update the full name the parent dashboard name - c.FullName = fmt.Sprintf("%s.%s.%s", c.Mod.ShortName, c.dashboard.UnqualifiedName, c.UnqualifiedName) + i.FullName = fmt.Sprintf("%s.%s.%s", i.Mod.ShortName, i.dashboard.UnqualifiedName, i.UnqualifiedName) // note: DO NOT update the unqualified name - this will be used in the parent dashboard selfInputsMap } + +// GetParams implements QueryProvider +func (i *DashboardInput) GetParams() []*ParamDef { + return i.Params +} + +// GetArgs implements QueryProvider +func (i *DashboardInput) GetArgs() *QueryArgs { + return i.Args + +} + +// GetSQL implements QueryProvider +func (i *DashboardInput) GetSQL() *string { + return i.SQL +} + +// GetQuery implements QueryProvider +func (i *DashboardInput) GetQuery() *Query { + return i.Query +} + +// SetArgs implements QueryProvider +func (i *DashboardInput) SetArgs(args *QueryArgs) { + i.Args = args +} + +// SetParams implements QueryProvider +func (i *DashboardInput) SetParams(params []*ParamDef) { + i.Params = params +} + +// GetPreparedStatementName implements QueryProvider +func (i *DashboardInput) GetPreparedStatementName() string { + if i.PreparedStatementName != "" { + return i.PreparedStatementName + } + i.PreparedStatementName = i.buildPreparedStatementName(i.ShortName, i.Mod.NameWithVersion(), constants.PreparedStatementInputSuffix) + return i.PreparedStatementName +} + +// GetPreparedStatementExecuteSQL implements QueryProvider +func (i *DashboardInput) GetPreparedStatementExecuteSQL(args *QueryArgs) (string, error) { + // defer to base + return i.getPreparedStatementExecuteSQL(i, args) +} diff --git a/steampipeconfig/modconfig/dashboard_table.go b/steampipeconfig/modconfig/dashboard_table.go index d5f3b97a..e6baa564 100644 --- a/steampipeconfig/modconfig/dashboard_table.go +++ b/steampipeconfig/modconfig/dashboard_table.go @@ -3,6 +3,8 @@ package modconfig import ( "fmt" + "github.com/turbot/steampipe/constants" + "github.com/turbot/steampipe/utils" "github.com/hashicorp/hcl/v2" @@ -14,6 +16,7 @@ import ( type DashboardTable struct { DashboardLeafNodeBase ResourceWithMetadataBase + QueryProviderBase // required to allow partial decoding Remain hcl.Body `hcl:",remain" json:"-"` @@ -237,9 +240,9 @@ func (t *DashboardTable) GetArgs() *QueryArgs { return t.Args } -// GetSQL implements QueryProvider, DashboardLeafNode -func (t *DashboardTable) GetSQL() string { - return typehelpers.SafeString(t.SQL) +// GetSQL implements QueryProvider +func (t *DashboardTable) GetSQL() *string { + return t.SQL } // GetQuery implements QueryProvider @@ -247,20 +250,6 @@ func (t *DashboardTable) GetQuery() *Query { return t.Query } -// GetPreparedStatementName implements QueryProvider -func (t *DashboardTable) GetPreparedStatementName() string { - // lazy load - if t.PreparedStatementName == "" { - t.PreparedStatementName = preparedStatementName(t) - } - return t.PreparedStatementName -} - -// GetModName implements QueryProvider -func (t *DashboardTable) GetModName() string { - return t.Mod.NameWithVersion() -} - // SetArgs implements QueryProvider func (t *DashboardTable) SetArgs(args *QueryArgs) { // nothing @@ -270,3 +259,18 @@ func (t *DashboardTable) SetArgs(args *QueryArgs) { func (t *DashboardTable) SetParams(params []*ParamDef) { t.Params = params } + +// GetPreparedStatementName implements QueryProvider +func (t *DashboardTable) GetPreparedStatementName() string { + if t.PreparedStatementName != "" { + return t.PreparedStatementName + } + t.PreparedStatementName = t.buildPreparedStatementName(t.ShortName, t.Mod.NameWithVersion(), constants.PreparedStatementTableSuffix) + return t.PreparedStatementName +} + +// GetPreparedStatementExecuteSQL implements QueryProvider +func (t *DashboardTable) GetPreparedStatementExecuteSQL(args *QueryArgs) (string, error) { + // defer to base + return t.getPreparedStatementExecuteSQL(t, args) +} diff --git a/steampipeconfig/modconfig/dashboard_text.go b/steampipeconfig/modconfig/dashboard_text.go index 5e615007..fbbcd54d 100644 --- a/steampipeconfig/modconfig/dashboard_text.go +++ b/steampipeconfig/modconfig/dashboard_text.go @@ -182,11 +182,6 @@ func (t *DashboardText) Diff(other *DashboardText) *DashboardTreeItemDiffs { return res } -// GetSQL implements DashboardLeafNode -func (t *DashboardText) GetSQL() string { - return "" -} - // GetWidth implements DashboardLeafNode func (t *DashboardText) GetWidth() int { if t.Width == nil { diff --git a/steampipeconfig/modconfig/interfaces.go b/steampipeconfig/modconfig/interfaces.go index f67cf373..baee7b65 100644 --- a/steampipeconfig/modconfig/interfaces.go +++ b/steampipeconfig/modconfig/interfaces.go @@ -10,7 +10,7 @@ import ( type MappableResource interface { // InitialiseFromFile creates a mappable resource from a file path // It returns the resource, and the raw file data - InitialiseFromFile(modPath, filePath string, mod *Mod) (MappableResource, []byte, error) + InitialiseFromFile(modPath, filePath string) (MappableResource, []byte, error) Name() string GetMetadata() *ResourceMetadata SetMetadata(*ResourceMetadata) @@ -54,13 +54,14 @@ type ResourceWithMetadata interface { // QueryProvider must be implemented by resources which supports prepared statements, i.e. Control and Query type QueryProvider interface { Name() string - GetModName() string + GetArgs() *QueryArgs GetParams() []*ParamDef - GetSQL() string + GetSQL() *string GetQuery() *Query - GetPreparedStatementName() string SetArgs(args *QueryArgs) SetParams(params []*ParamDef) + GetPreparedStatementName() string + GetPreparedStatementExecuteSQL(args *QueryArgs) (string, error) } // ParameterisedDashboardNode must be implemented by resources has params and args @@ -69,15 +70,16 @@ type ParameterisedDashboardNode interface { GetArgs() *QueryArgs } -// DashboardLeafNode must be implemented by resources may be a leaf node in the repoort execution tree +// DashboardLeafNode must be implemented by resources may be a leaf node in the dashboard execution tree type DashboardLeafNode interface { Name() string GetUnqualifiedName() string GetTitle() string GetWidth() int GetPaths() []NodePath - GetSQL() string + // implemented by DashboardLeafNodeBase + AddRuntimeDependencies(*RuntimeDependency) GetRuntimeDependencies() map[string]*RuntimeDependency } diff --git a/steampipeconfig/modconfig/mod.go b/steampipeconfig/modconfig/mod.go index fd21247b..da22fa6e 100644 --- a/steampipeconfig/modconfig/mod.go +++ b/steampipeconfig/modconfig/mod.go @@ -118,7 +118,7 @@ func NewMod(shortName, modPath string, defRange hcl.Range) (*Mod, error) { } func (m *Mod) PopulateResourceMaps() { - m.resourceMaps = WorkspaceResourceMapFromMod(m) + m.resourceMaps = CreateWorkspaceResourceMapForMod(m) } func (m *Mod) setVersion() { diff --git a/steampipeconfig/modconfig/prepared_statement.go b/steampipeconfig/modconfig/prepared_statement.go deleted file mode 100644 index cf4793a0..00000000 --- a/steampipeconfig/modconfig/prepared_statement.go +++ /dev/null @@ -1,57 +0,0 @@ -package modconfig - -import ( - "fmt" - "log" - "strings" - - "github.com/turbot/steampipe/utils" -) - -const maxPreparedStatementNameLength = 63 -const preparesStatementQuerySuffix = "_q" -const preparesStatementControlSuffix = "_c" - -// GetPreparedStatementExecuteSQL return the SQLs to run the query as a prepared statement -func GetPreparedStatementExecuteSQL(source QueryProvider, args *QueryArgs) (string, error) { - paramsString, err := args.ResolveAsString(source) - if err != nil { - return "", fmt.Errorf("failed to resolve args for %s: %s", source.Name(), err.Error()) - } - executeString := fmt.Sprintf("execute %s%s", source.GetPreparedStatementName(), paramsString) - log.Printf("[TRACE] GetPreparedStatementExecuteSQL source: %s, sql: %s, args: %s", source.Name(), executeString, args) - return executeString, nil -} - -func preparedStatementName(source QueryProvider) string { - var name, suffix string - prefix := fmt.Sprintf("%s_", source.GetModName()) - prefix = strings.Replace(prefix, ".", "_", -1) - prefix = strings.Replace(prefix, "@", "_", -1) - - // build suffix using a char to indicate control or query, and the truncated hash - switch t := source.(type) { - case *Query: - name = t.ShortName - suffix = preparesStatementQuerySuffix - case *Control: - name = t.ShortName - suffix = preparesStatementControlSuffix - } - // build the hash from the query/control name, mod name and suffix and take the first 4 bytes - str := fmt.Sprintf("%s%s%s", prefix, name, suffix) - hash := utils.GetMD5Hash(str)[:4] - // add hash to suffix - suffix += hash - - // truncate the name if necessary - nameLength := len(name) - maxNameLength := maxPreparedStatementNameLength - (len(prefix) + len(suffix)) - if nameLength > maxNameLength { - nameLength = maxNameLength - } - - // construct the name - preparedStatementName := fmt.Sprintf("%s%s%s", prefix, name[:nameLength], suffix) - return preparedStatementName -} diff --git a/steampipeconfig/modconfig/query.go b/steampipeconfig/modconfig/query.go index 828b186c..7a16bc4e 100644 --- a/steampipeconfig/modconfig/query.go +++ b/steampipeconfig/modconfig/query.go @@ -18,6 +18,7 @@ import ( // Query is a struct representing the Query resource type Query struct { ResourceWithMetadataBase + QueryProviderBase // required to allow partial decoding Remain hcl.Body `hcl:",remain"` @@ -25,27 +26,29 @@ type Query struct { ShortName string `cty:"short_name"` FullName string `cty:"name"` - Description *string `cty:"description" hcl:"description" column:"description,text"` - Documentation *string `cty:"documentation" hcl:"documentation" column:"documentation,text"` - SearchPath *string `cty:"search_path" hcl:"search_path" column:"search_path,text"` - SearchPathPrefix *string `cty:"search_path_prefix" hcl:"search_path_prefix" column:"search_path_prefix,text"` - SQL *string `cty:"sql" hcl:"sql" column:"sql,text"` - Tags map[string]string `cty:"tags" hcl:"tags,optional" column:"tags,jsonb"` - Title *string `cty:"title" hcl:"title" column:"title,text"` + Description *string `cty:"description" hcl:"description" column:"description,text"` + Documentation *string `cty:"documentation" hcl:"documentation" column:"documentation,text"` + SearchPath *string `cty:"search_path" hcl:"search_path" column:"search_path,text"` + SearchPathPrefix *string `cty:"search_path_prefix" hcl:"search_path_prefix" column:"search_path_prefix,text"` + Tags map[string]string `cty:"tags" hcl:"tags,optional" column:"tags,jsonb"` + Title *string `cty:"title" hcl:"title" column:"title,text"` + PreparedStatementName string `column:"prepared_statement_name,text" json:"-"` + SQL *string `cty:"sql" hcl:"sql" column:"sql,text"` Params []*ParamDef `cty:"params" column:"params,jsonb"` // list of all blocks referenced by the resource References []*ResourceReference - Mod *Mod `cty:"mod"` - DeclRange hcl.Range - PreparedStatementName string `column:"prepared_statement_name,text"` - UnqualifiedName string - Paths []NodePath `column:"path,jsonb"` - parents []ModTreeItem + Mod *Mod `cty:"mod"` + DeclRange hcl.Range + + UnqualifiedName string + Paths []NodePath `column:"path,jsonb"` + parents []ModTreeItem } func NewQuery(block *hcl.Block, mod *Mod) *Query { + // queries cannot be anonymous shortName := block.Labels[0] q := &Query{ ShortName: shortName, @@ -57,6 +60,43 @@ func NewQuery(block *hcl.Block, mod *Mod) *Query { return q } +func QueryFromFile(modPath, filePath string, mod *Mod) (MappableResource, []byte, error) { + q := &Query{ + Mod: mod, + } + return q.InitialiseFromFile(modPath, filePath) +} + +// InitialiseFromFile implements MappableResource +func (q *Query) InitialiseFromFile(modPath, filePath string) (MappableResource, []byte, error) { + // only valid for sql files + if filepath.Ext(filePath) != constants.SqlExtension { + return nil, nil, fmt.Errorf("Query.InitialiseFromFile must be called with .sql files only - filepath: '%s'", filePath) + } + + sqlBytes, err := os.ReadFile(filePath) + if err != nil { + return nil, nil, err + } + + sql := string(sqlBytes) + if sql == "" { + log.Printf("[TRACE] SQL file %s contains no query", filePath) + return nil, nil, nil + } + // get a sluggified version of the filename + name, err := PseudoResourceNameFromPath(modPath, filePath) + if err != nil { + return nil, nil, err + } + q.ShortName = name + q.UnqualifiedName = fmt.Sprintf("query.%s", name) + q.FullName = fmt.Sprintf("%s.query.%s", q.Mod.ShortName, name) + q.SQL = &sql + + return q, sqlBytes, nil +} + func (q *Query) Equals(other *Query) bool { res := q.ShortName == other.ShortName && q.FullName == other.FullName && @@ -124,41 +164,6 @@ func (q *Query) String() string { return res } -func QueryFromFile(modPath, filePath string, mod *Mod) (MappableResource, []byte, error) { - q := &Query{} - return q.InitialiseFromFile(modPath, filePath, mod) -} - -// InitialiseFromFile implements MappableResource -func (q *Query) InitialiseFromFile(modPath, filePath string, mod *Mod) (MappableResource, []byte, error) { - // only valid for sql files - if filepath.Ext(filePath) != constants.SqlExtension { - return nil, nil, fmt.Errorf("Query.InitialiseFromFile must be called with .sql files only - filepath: '%s'", filePath) - } - - sqlBytes, err := os.ReadFile(filePath) - if err != nil { - return nil, nil, err - } - - sql := string(sqlBytes) - if sql == "" { - log.Printf("[TRACE] SQL file %s contains no query", filePath) - return nil, nil, nil - } - // get a sluggified version of the filename - name, err := PseudoResourceNameFromPath(modPath, filePath) - if err != nil { - return nil, nil, err - } - q.ShortName = name - q.UnqualifiedName = fmt.Sprintf("query.%s", name) - q.FullName = fmt.Sprintf("%s.query.%s", mod.ShortName, name) - q.SQL = &sql - q.Mod = mod - return q, sqlBytes, nil -} - // Name implements MappableResource, HclResource func (q *Query) Name() string { return q.FullName @@ -197,28 +202,14 @@ func (q *Query) GetArgs() *QueryArgs { return nil } -// GetSQL implements QueryProvider, DashboardLeafNode -func (q *Query) GetSQL() string { - return typehelpers.SafeString(q.SQL) -} - // GetQuery implements QueryProvider func (q *Query) GetQuery() *Query { return nil } -// GetPreparedStatementName implements QueryProvider -func (q *Query) GetPreparedStatementName() string { - // lazy load - if q.PreparedStatementName == "" { - q.PreparedStatementName = preparedStatementName(q) - } - return q.PreparedStatementName -} - -// GetModName implements QueryProvider -func (q *Query) GetModName() string { - return q.Mod.NameWithVersion() +// GetSQL implements QueryProvider +func (q *Query) GetSQL() *string { + return q.SQL } // SetArgs implements QueryProvider @@ -231,6 +222,21 @@ func (q *Query) SetParams(params []*ParamDef) { q.Params = params } +// GetPreparedStatementName implements QueryProvider +func (q *Query) GetPreparedStatementName() string { + if q.PreparedStatementName != "" { + return q.PreparedStatementName + } + q.PreparedStatementName = q.buildPreparedStatementName(q.ShortName, q.Mod.NameWithVersion(), constants.PreparedStatementQuerySuffix) + return q.PreparedStatementName +} + +// GetPreparedStatementExecuteSQL implements QueryProvider +func (q *Query) GetPreparedStatementExecuteSQL(args *QueryArgs) (string, error) { + // defer to base + return q.getPreparedStatementExecuteSQL(q, args) +} + // AddParent implements ModTreeItem func (q *Query) AddParent(parent ModTreeItem) error { q.parents = append(q.parents, parent) diff --git a/steampipeconfig/modconfig/query_provider_base.go b/steampipeconfig/modconfig/query_provider_base.go new file mode 100644 index 00000000..5410e626 --- /dev/null +++ b/steampipeconfig/modconfig/query_provider_base.go @@ -0,0 +1,55 @@ +package modconfig + +import ( + "fmt" + "log" + "strings" + + "github.com/turbot/steampipe/constants" + "github.com/turbot/steampipe/utils" +) + +type QueryProviderBase struct { +} + +func (p *QueryProviderBase) buildPreparedStatementName(queryName, modName, suffix string) string { + // build prefix from mod name + prefix := p.buildPreparedStatementPrefix(modName) + + // build the hash from the query/control name, mod name and suffix and take the first 4 bytes + str := fmt.Sprintf("%s%s%s", prefix, queryName, suffix) + hash := utils.GetMD5Hash(str)[:4] + // add hash to suffix + suffix += hash + + // truncate the name if necessary + nameLength := len(queryName) + maxNameLength := constants.MaxPreparedStatementNameLength - (len(prefix) + len(suffix)) + if nameLength > maxNameLength { + nameLength = maxNameLength + } + + // construct the name + return fmt.Sprintf("%s%s%s", prefix, queryName[:nameLength], suffix) +} + +// set the prepared statement suffix and prefix +// and also store the parent resource object as a QueryProvider interface (base struct cannot cast itself to this) +func (p *QueryProviderBase) buildPreparedStatementPrefix(modName string) string { + prefix := fmt.Sprintf("%s_", modName) + prefix = strings.Replace(prefix, ".", "_", -1) + prefix = strings.Replace(prefix, "@", "_", -1) + + return prefix +} + +// return the SQLs to run the query as a prepared statement +func (p *QueryProviderBase) getPreparedStatementExecuteSQL(queryProvider QueryProvider, args *QueryArgs) (string, error) { + paramsString, err := args.ResolveAsString(queryProvider) + if err != nil { + return "", fmt.Errorf("failed to resolve args for %s: %s", queryProvider.Name(), err.Error()) + } + executeString := fmt.Sprintf("execute %s%s", queryProvider.GetPreparedStatementName(), paramsString) + log.Printf("[TRACE] GetPreparedStatementExecuteSQL source: %s, sql: %s, args: %s", queryProvider.Name(), executeString, args) + return executeString, nil +} diff --git a/steampipeconfig/modconfig/workspace_resource_maps.go b/steampipeconfig/modconfig/workspace_resource_maps.go index 8c5e0360..63fef85c 100644 --- a/steampipeconfig/modconfig/workspace_resource_maps.go +++ b/steampipeconfig/modconfig/workspace_resource_maps.go @@ -27,7 +27,7 @@ type WorkspaceResourceMaps struct { LocalBenchmarks map[string]*Benchmark } -func WorkspaceResourceMapFromMod(mod *Mod) *WorkspaceResourceMaps { +func CreateWorkspaceResourceMapForMod(mod *Mod) *WorkspaceResourceMaps { resourceMaps := &WorkspaceResourceMaps{ Mod: mod, Mods: make(map[string]*Mod), @@ -45,12 +45,71 @@ func WorkspaceResourceMapFromMod(mod *Mod) *WorkspaceResourceMaps { DashboardTables: mod.DashboardTables, DashboardTexts: mod.DashboardTexts, } + // if mod is not a default mod (i.e. if there is a mod.sp), add it into the resource maps if !mod.IsDefaultMod() { resourceMaps.Mods[mod.Name()] = mod } + return resourceMaps } +func CreateWorkspaceResourceMapForQueryProviders(queryProviders []QueryProvider) *WorkspaceResourceMaps { + res := &WorkspaceResourceMaps{ + Mods: make(map[string]*Mod), + Queries: make(map[string]*Query), + Controls: make(map[string]*Control), + Benchmarks: make(map[string]*Benchmark), + Variables: make(map[string]*Variable), + References: make(map[string]*ResourceReference), + } + for _, p := range queryProviders { + res.addQueryProvider(p) + } + return res +} + +func (m *WorkspaceResourceMaps) QueryProviders() []QueryProvider { + res := make([]QueryProvider, + len(m.Queries)+ + len(m.Controls)+ + len(m.DashboardCards)+ + len(m.DashboardCharts)+ + len(m.DashboardHierarchies)+ + len(m.DashboardInputs)+ + len(m.DashboardTables)) + + idx := 0 + for _, p := range m.Queries { + res[idx] = p + idx++ + } + for _, p := range m.Controls { + res[idx] = p + idx++ + } + for _, p := range m.DashboardCards { + res[idx] = p + idx++ + } + for _, p := range m.DashboardCharts { + res[idx] = p + idx++ + } + for _, p := range m.DashboardHierarchies { + res[idx] = p + idx++ + } + for _, p := range m.DashboardInputs { + res[idx] = p + idx++ + } + for _, p := range m.DashboardTables { + res[idx] = p + idx++ + } + return res +} + func (m *WorkspaceResourceMaps) Equals(other *WorkspaceResourceMaps) bool { if other == nil { return false @@ -242,19 +301,6 @@ func (m *WorkspaceResourceMaps) Equals(other *WorkspaceResourceMaps) bool { return true } -func (m *WorkspaceResourceMaps) AddQueryProvider(provider QueryProvider) { - switch p := provider.(type) { - case *Query: - if p != nil { - m.Queries[p.FullName] = p - } - case *Control: - if p != nil { - m.Controls[p.FullName] = p - } - } -} - func (m *WorkspaceResourceMaps) PopulateReferences() { m.References = make(map[string]*ResourceReference) for _, mod := range m.Mods { @@ -301,3 +347,32 @@ func (m *WorkspaceResourceMaps) Empty() bool { len(m.DashboardTexts)+ len(m.References) == 0 } + +func (m *WorkspaceResourceMaps) addQueryProvider(provider QueryProvider) { + switch p := provider.(type) { + case *Query: + if p != nil { + m.Queries[p.FullName] = p + } + case *Control: + if p != nil { + m.Controls[p.FullName] = p + } + case *DashboardCard: + if p != nil { + m.DashboardCards[p.FullName] = p + } + case *DashboardChart: + if p != nil { + m.DashboardCharts[p.FullName] = p + } + case *DashboardHierarchy: + if p != nil { + m.DashboardHierarchies[p.FullName] = p + } + case *DashboardInput: + if p != nil { + m.DashboardInputs[p.FullName] = p + } + } +} diff --git a/steampipeconfig/parse/decode.go b/steampipeconfig/parse/decode.go index d0221f5a..3bc09c9a 100644 --- a/steampipeconfig/parse/decode.go +++ b/steampipeconfig/parse/decode.go @@ -295,7 +295,7 @@ func decodeQueryProvider(block *hcl.Block, parent modconfig.ModTreeItem, runCtx return nil, res } - // decodee the body into 'resource' to populate all properties that can be automatically decoded + // decode the body into 'resource' to populate all properties that can be automatically decoded diags = gohcl.DecodeBody(block.Body, runCtx.EvalCtx, resource) // handle any resulting diags, which may specify dependencies res.handleDecodeDiags(content, resource, diags) @@ -307,16 +307,18 @@ func decodeQueryProvider(block *hcl.Block, parent modconfig.ModTreeItem, runCtx panic(fmt.Sprintf("block type %s not convertible to a query provider", block.Type)) } - if queryProvider.GetQuery() != nil && queryProvider.GetSQL() != "" { - if attr, exists := content.Attributes["query"]; exists { - // either Query or SQL property may be set - if Query property already set, error - diags = append(diags, &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: fmt.Sprintf("%s has both 'SQL' and 'query' property set - only 1 of these may be set", resource.Name()), - Subject: &attr.Range, - }) - } - res.handleDecodeDiags(content, resource, diags) + sqlAttr, sqlPropertySet := content.Attributes["sql"] + _, queryPropertySet := content.Attributes["query"] + + if sqlPropertySet && queryPropertySet { + // either Query or SQL property may be set - if Query property already set, error + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("%s has both 'SQL' and 'query' property set - only 1 of these may be set", resource.Name()), + Subject: &sqlAttr.Range, + }) + + res.addDiags(diags) } if attr, exists := content.Attributes["args"]; exists { @@ -337,7 +339,7 @@ func decodeQueryProvider(block *hcl.Block, parent modconfig.ModTreeItem, runCtx } // param block cannot be set if a query property is set - it is only valid if inline SQL ids defined - if queryProvider.GetQuery() != nil { + if queryPropertySet { diags = append(diags, invalidParamDiags(resource, block)) } paramDef, moreDiags := decodeParam(block, runCtx, resource.Name()) diff --git a/steampipeconfig/testdata/mods/report_test/dashboard.sp b/steampipeconfig/testdata/mods/report_test/dashboard.sp index 438209b6..916c7872 100644 --- a/steampipeconfig/testdata/mods/report_test/dashboard.sp +++ b/steampipeconfig/testdata/mods/report_test/dashboard.sp @@ -16,6 +16,10 @@ dashboard "r1"{ } } + control { + query = query.q1 + } + } //dashboard "r2"{ diff --git a/workspace/session_data_source.go b/workspace/session_data_source.go index dba70256..72ecb7fd 100644 --- a/workspace/session_data_source.go +++ b/workspace/session_data_source.go @@ -9,6 +9,11 @@ type SessionDataSource struct { IntrospectionTableSource func() *modconfig.WorkspaceResourceMaps } +// TODO [report] change this to accept a list of queries instead of and create the cut down preparedStatementSource here +// NewSessionDataSource uses the workspace and (optionally) a separate the prepared statemeot source +// and returns a SessionDataSource +// NOTE: preparedStatementSource is only set if specific queries have ben passed to the query command +// it allows us to only create the prepared statements me need func NewSessionDataSource(w *Workspace, preparedStatementSource *modconfig.WorkspaceResourceMaps) *SessionDataSource { res := &SessionDataSource{ IntrospectionTableSource: func() *modconfig.WorkspaceResourceMaps { @@ -18,7 +23,7 @@ func NewSessionDataSource(w *Workspace, preparedStatementSource *modconfig.Works return w.GetResourceMaps() }, } - if preparedStatementSource != nil && !preparedStatementSource.Empty() { + if preparedStatementSource != nil { res.PreparedStatementSource = func() *modconfig.WorkspaceResourceMaps { return preparedStatementSource } diff --git a/workspace/workspace.go b/workspace/workspace.go index cd5aabb8..1a72c256 100644 --- a/workspace/workspace.go +++ b/workspace/workspace.go @@ -28,27 +28,8 @@ type Workspace struct { ModInstallationPath string Mod *modconfig.Mod - // maps of mod resources from this mod and ALL DEPENDENCIES, keyed by long and short names - - Queries map[string]*modconfig.Query - Controls map[string]*modconfig.Control - Benchmarks map[string]*modconfig.Benchmark - Mods map[string]*modconfig.Mod - Dashboards map[string]*modconfig.Dashboard - DashboardContainers map[string]*modconfig.DashboardContainer - DashboardCards map[string]*modconfig.DashboardCard - DashboardCharts map[string]*modconfig.DashboardChart - DashboardHierarchies map[string]*modconfig.DashboardHierarchy - DashboardImages map[string]*modconfig.DashboardImage - DashboardInputs map[string]*modconfig.DashboardInput - DashboardTables map[string]*modconfig.DashboardTable - DashboardTexts map[string]*modconfig.DashboardText - Variables map[string]*modconfig.Variable - - //local resources keyed by unqualified name - LocalQueries map[string]*modconfig.Query - LocalControls map[string]*modconfig.Control - LocalBenchmarks map[string]*modconfig.Benchmark + Mods map[string]*modconfig.Mod + Variables map[string]*modconfig.Variable watcher *utils.FileWatcher loadLock sync.Mutex @@ -63,7 +44,8 @@ type Workspace struct { onFileWatcherEventMessages func() modFileExists bool loadPseudoResources bool - // convenient aggregation of all resources + + // maps of mod resources from this mod and ALL DEPENDENCIES, keyed by long and short names resourceMaps *modconfig.WorkspaceResourceMaps } @@ -164,26 +146,6 @@ func (w *Workspace) Close() { } } -// clear all resource maps -func (w *Workspace) reset() { - w.Queries = make(map[string]*modconfig.Query) - w.Controls = make(map[string]*modconfig.Control) - w.Benchmarks = make(map[string]*modconfig.Benchmark) - w.Mods = make(map[string]*modconfig.Mod) - w.Dashboards = make(map[string]*modconfig.Dashboard) - w.DashboardContainers = make(map[string]*modconfig.DashboardContainer) - w.DashboardCards = make(map[string]*modconfig.DashboardCard) - w.DashboardCharts = make(map[string]*modconfig.DashboardChart) - w.DashboardHierarchies = make(map[string]*modconfig.DashboardHierarchy) - w.DashboardImages = make(map[string]*modconfig.DashboardImage) - w.DashboardInputs = make(map[string]*modconfig.DashboardInput) - w.DashboardTables = make(map[string]*modconfig.DashboardTable) - w.DashboardTexts = make(map[string]*modconfig.DashboardText) - w.LocalQueries = make(map[string]*modconfig.Query) - w.LocalControls = make(map[string]*modconfig.Control) - w.LocalBenchmarks = make(map[string]*modconfig.Benchmark) -} - // check whether the workspace contains a modfile // this will determine whether we load files recursively, and create pseudo resources for sql files func (w *Workspace) setModfileExists() { @@ -204,8 +166,6 @@ func (w *Workspace) setModfileExists() { } func (w *Workspace) loadWorkspaceMod(ctx context.Context) error { - // clear all resource maps - w.reset() // load and evaluate all variables inputVariables, err := w.getAllVariables(ctx) if err != nil { @@ -229,19 +189,6 @@ func (w *Workspace) loadWorkspaceMod(ctx context.Context) error { // now set workspace properties w.Mod = m - w.Queries, w.LocalQueries = w.buildQueryMap(runCtx.LoadedDependencyMods) - w.Controls, w.LocalControls = w.buildControlMap(runCtx.LoadedDependencyMods) - w.Benchmarks, w.LocalBenchmarks = w.buildBenchmarkMap(runCtx.LoadedDependencyMods) - w.Dashboards = w.buildDashboardMap(runCtx.LoadedDependencyMods) - w.DashboardContainers = w.buildDashboardContainerMap(runCtx.LoadedDependencyMods) - w.DashboardCards = w.buildDashboardCardMap(runCtx.LoadedDependencyMods) - w.DashboardCharts = w.buildDashboardChartMap(runCtx.LoadedDependencyMods) - w.DashboardHierarchies = w.buildDashboardHierarchyMap(runCtx.LoadedDependencyMods) - w.DashboardImages = w.buildDashboardImageMap(runCtx.LoadedDependencyMods) - w.DashboardInputs = w.buildDashboardInputMap(runCtx.LoadedDependencyMods) - w.DashboardTables = w.buildDashboardTableMap(runCtx.LoadedDependencyMods) - w.DashboardTexts = w.buildDashboardTextMap(runCtx.LoadedDependencyMods) - // set variables on workspace w.Variables = m.Variables w.Mods = runCtx.LoadedDependencyMods @@ -341,7 +288,7 @@ func (w *Workspace) loadWorkspaceResourceName() (*modconfig.WorkspaceResources, } func (w *Workspace) verifyResourceRuntimeDependencies() error { - for _, d := range w.Dashboards { + for _, d := range w.resourceMaps.Dashboards { if err := d.BuildRuntimeDependencyTree(w); err != nil { return err } diff --git a/workspace/workspace_events.go b/workspace/workspace_events.go index dd90b755..ac15986d 100644 --- a/workspace/workspace_events.go +++ b/workspace/workspace_events.go @@ -6,8 +6,8 @@ import ( "strings" "github.com/fsnotify/fsnotify" - "github.com/turbot/steampipe/db/db_common" "github.com/turbot/steampipe/dashboard/dashboardevents" + "github.com/turbot/steampipe/db/db_common" "github.com/turbot/steampipe/steampipeconfig/modconfig" "github.com/turbot/steampipe/utils" ) @@ -53,7 +53,7 @@ func (w *Workspace) reloadResourceMaps(ctx context.Context) (*modconfig.Workspac prevResourceMaps := w.resourceMaps // if there is an outsanding watcher error, set prevResourceMaps to empty to force refresh if w.watcherError != nil { - prevResourceMaps = modconfig.WorkspaceResourceMapFromMod(w.Mod) + prevResourceMaps = modconfig.CreateWorkspaceResourceMapForMod(w.Mod) } // now reload the workspace diff --git a/workspace/workspace_queries.go b/workspace/workspace_queries.go index bd6c9ba4..ef4434da 100644 --- a/workspace/workspace_queries.go +++ b/workspace/workspace_queries.go @@ -20,8 +20,8 @@ func (w *Workspace) GetQueriesFromArgs(args []string) ([]string, *modconfig.Work defer utils.LogTime("execute.GetQueriesFromArgs end") var queries []string - // build map of prepared statement providers - var resourceMap = modconfig.WorkspaceResourceMapFromMod(w.Mod) + var queryProviders []modconfig.QueryProvider + // build map of just the required prepared statement providers for _, arg := range args { query, queryProvider, err := w.ResolveQueryAndArgs(arg) if err != nil { @@ -29,10 +29,15 @@ func (w *Workspace) GetQueriesFromArgs(args []string) ([]string, *modconfig.Work } if len(query) > 0 { queries = append(queries, query) - resourceMap.AddQueryProvider(queryProvider) + queryProviders = append(queryProviders, queryProvider) + } } - return queries, resourceMap, nil + var preparedStatementSource *modconfig.WorkspaceResourceMaps + if len(queries) > 0 { + preparedStatementSource = modconfig.CreateWorkspaceResourceMapForQueryProviders(queryProviders) + } + return queries, preparedStatementSource, nil } // ResolveQueryAndArgs attempts to resolve 'arg' to a query and query args @@ -52,19 +57,22 @@ func (w *Workspace) ResolveQueryAndArgs(sqlString string) (string, modconfig.Que return w.resolveQuery(sqlString, args) } -// ResolveControlQuery resolves the query for the given Control -func (w *Workspace) ResolveControlQuery(control *modconfig.Control, args *modconfig.QueryArgs) (string, error) { - args, err := w.resolveControlArgs(control, args) +// ResolveQuery resolves the query for the given QueryProvider +func (w *Workspace) ResolveQuery(queryProvider modconfig.QueryProvider, args *modconfig.QueryArgs) (string, error) { + args, err := w.resolveQueryProviderArgs(queryProvider, args) if err != nil { return "", err } - log.Printf("[TRACE] ResolveControlQuery for %s", control.FullName) + log.Printf("[TRACE] ResolveQuery for %s", queryProvider.Name()) + query := queryProvider.GetQuery() + sql := queryProvider.GetSQL() + params := queryProvider.GetParams() // verify we have either SQL or a Query defined - if control.SQL == nil && control.Query == nil { + if sql == nil && query == nil { // this should never happen as we should catch it in the parsing stage - return "", fmt.Errorf("%s must define either a 'sql' property or a 'query' property", control.FullName) + return "", fmt.Errorf("%s must define either a 'sql' property or a 'query' property", queryProvider.Name()) } // determine the source for the query @@ -72,68 +80,65 @@ func (w *Workspace) ResolveControlQuery(control *modconfig.Control, args *modcon // either via its SQL property (passing a query name) or Query property (using a reference to a query object) // if a query is provided, us that to resolve the sql - if control.Query != nil { - return w.resolveNamedQuery(control.Query, args) + if query != nil { + return w.resolveNamedQuery(query, args) } // if the control has SQL set, use that - if control.SQL != nil { - controlSQL := typehelpers.SafeString(control.SQL) + if sql != nil { + queryProviderSQL := typehelpers.SafeString(sql) log.Printf("[TRACE] control defines inline SQL") // if the control SQL refers to a named query, this is the same as if the control 'Query' property is set - if namedQuery, ok := w.GetQuery(controlSQL); ok { + if namedQuery, ok := w.GetQuery(queryProviderSQL); ok { // in this case, it is NOT valid for the control to define its own Param definitions - if control.Params != nil { - return "", fmt.Errorf("%s has an 'SQL' property which refers to %s, so it cannot define 'param' blocks", control.FullName, namedQuery.FullName) + if params != nil { + return "", fmt.Errorf("%s has an 'SQL' property which refers to %s, so it cannot define 'param' blocks", queryProvider.Name(), namedQuery.FullName) } return w.resolveNamedQuery(namedQuery, args) } // so the control sql is NOT a named query // if there are NO params, use the control SQL as is - if len(control.Params) == 0 { - return controlSQL, nil + if len(params) == 0 { + return queryProviderSQL, nil } // so the control sql is NOT a named query // if there are NO params, use the control SQL as is - if len(control.Params) == 0 { - return controlSQL, nil + if len(params) == 0 { + return queryProviderSQL, nil } } // so the control defines SQL and has params - it is a prepared statement - return modconfig.GetPreparedStatementExecuteSQL(control, args) + return queryProvider.GetPreparedStatementExecuteSQL(args) } func (w *Workspace) resolveQuery(sqlString string, args *modconfig.QueryArgs) (string, modconfig.QueryProvider, error) { // query or control providing the named query - var queryProvider modconfig.QueryProvider - log.Printf("[TRACE] resolveQuery %s args %s", sqlString, args) // 1) check if this is a control if control, ok := w.GetControl(sqlString); ok { - queryProvider = control + log.Printf("[TRACE] query string is a control: %s", control.FullName) // copy control SQL into query and continue resolution var err error - sqlString, err = w.ResolveControlQuery(control, args) + sqlString, err = w.ResolveQuery(control, args) if err != nil { return "", nil, err } log.Printf("[TRACE] resolved control query: %s", sqlString) - return sqlString, queryProvider, nil + return sqlString, control, nil } // 2) is this a named query if namedQuery, ok := w.GetQuery(sqlString); ok { - queryProvider = namedQuery sql, err := w.resolveNamedQuery(namedQuery, args) if err != nil { return "", nil, err } - return sql, queryProvider, nil + return sql, namedQuery, nil } // 3) is this a file @@ -146,7 +151,7 @@ func (w *Workspace) resolveQuery(sqlString string, args *modconfig.QueryArgs) (s utils.ShowWarning(fmt.Sprintf("file '%s' does not contain any data", sqlString)) // (just return the empty string - it will be filtered above) } - return fileQuery, queryProvider, nil + return fileQuery, nil, nil } // 4) so we have not managed to resolve this - if it looks like a named query or control, return an error @@ -155,7 +160,7 @@ func (w *Workspace) resolveQuery(sqlString string, args *modconfig.QueryArgs) (s } // 5) just use the query string as is and assume it is valid SQL - return sqlString, queryProvider, nil + return sqlString, nil, nil } func (w *Workspace) resolveNamedQuery(namedQuery *modconfig.Query, args *modconfig.QueryArgs) (string, error) { @@ -165,23 +170,23 @@ func (w *Workspace) resolveNamedQuery(namedQuery *modconfig.Query, args *modconf } // so there are params - this will be a prepared statement - sql, err := modconfig.GetPreparedStatementExecuteSQL(namedQuery, args) + sql, err := namedQuery.GetPreparedStatementExecuteSQL(args) if err != nil { return "", err } return sql, nil } -func (w *Workspace) resolveControlArgs(control *modconfig.Control, args *modconfig.QueryArgs) (*modconfig.QueryArgs, error) { +func (w *Workspace) resolveQueryProviderArgs(queryProvider modconfig.QueryProvider, args *modconfig.QueryArgs) (*modconfig.QueryArgs, error) { // if no args were provided, set args to control args (which may also be nil!) if args == nil || args.Empty() { - log.Printf("[TRACE] using control args: %s", control.Args) - return control.Args, nil + log.Printf("[TRACE] using control args: %s", queryProvider.GetArgs()) + return queryProvider.GetArgs(), nil } // so command line args were provided // check if the control supports them (it will NOT is it specifies a 'query' property) - if control.Query != nil { - return nil, fmt.Errorf("%s defines a query property and so does not support command line arguments", control.FullName) + if queryProvider.GetQuery() != nil { + return nil, fmt.Errorf("%s defines a query property and so does not support command line arguments", queryProvider.Name()) } log.Printf("[TRACE] using command line args: %s", args) diff --git a/workspace/workspace_resources.go b/workspace/workspace_resources.go index 89a56fa3..abfbd37f 100644 --- a/workspace/workspace_resources.go +++ b/workspace/workspace_resources.go @@ -6,10 +6,10 @@ func (w *Workspace) GetQuery(queryName string) (*modconfig.Query, bool) { w.loadLock.Lock() defer w.loadLock.Unlock() - if query, ok := w.LocalQueries[queryName]; ok { + if query, ok := w.resourceMaps.LocalQueries[queryName]; ok { return query, true } - if query, ok := w.Queries[queryName]; ok { + if query, ok := w.resourceMaps.Queries[queryName]; ok { return query, true } return nil, false @@ -19,10 +19,10 @@ func (w *Workspace) GetControl(controlName string) (*modconfig.Control, bool) { w.loadLock.Lock() defer w.loadLock.Unlock() - if control, ok := w.LocalControls[controlName]; ok { + if control, ok := w.resourceMaps.LocalControls[controlName]; ok { return control, true } - if control, ok := w.Controls[controlName]; ok { + if control, ok := w.resourceMaps.Controls[controlName]; ok { return control, true } return nil, false @@ -37,35 +37,40 @@ func (w *Workspace) GetResourceMaps() *modconfig.WorkspaceResourceMaps { } func (w *Workspace) populateResourceMaps() { + queries, localQueries := w.buildQueryMap() + controls, localControls := w.buildControlMap() + benchmarks, localBenchmarks := w.buildBenchmarkMap() + w.resourceMaps = &modconfig.WorkspaceResourceMaps{ Mod: w.Mod, Mods: make(map[string]*modconfig.Mod), - LocalQueries: w.LocalQueries, - Queries: w.Queries, - Controls: w.Controls, - LocalControls: w.LocalControls, - Benchmarks: w.Benchmarks, - LocalBenchmarks: w.LocalBenchmarks, + LocalQueries: localQueries, + Queries: queries, + Controls: controls, + LocalControls: localControls, + Benchmarks: benchmarks, + LocalBenchmarks: localBenchmarks, Variables: w.Variables, - Dashboards: w.Dashboards, - DashboardContainers: w.DashboardContainers, - DashboardCards: w.DashboardCards, - DashboardCharts: w.DashboardCharts, - DashboardHierarchies: w.DashboardHierarchies, - DashboardImages: w.DashboardImages, - DashboardInputs: w.DashboardInputs, - DashboardTables: w.DashboardTables, - DashboardTexts: w.DashboardTexts, + Dashboards: w.buildDashboardMap(), + DashboardContainers: w.buildDashboardContainerMap(), + DashboardCards: w.buildDashboardCardMap(), + DashboardCharts: w.buildDashboardChartMap(), + DashboardHierarchies: w.buildDashboardHierarchyMap(), + DashboardImages: w.buildDashboardImageMap(), + DashboardInputs: w.buildDashboardInputMap(), + DashboardTables: w.buildDashboardTableMap(), + DashboardTexts: w.buildDashboardTextMap(), } w.resourceMaps.PopulateReferences() - + // if mod is not a default mod (i.e. if there is a mod.sp), add it into the resource maps if !w.Mod.IsDefaultMod() { w.resourceMaps.Mods[w.Mod.Name()] = w.Mod } + } // resource map building -func (w *Workspace) buildQueryMap(modMap modconfig.ModMap) (map[string]*modconfig.Query, map[string]*modconfig.Query) { +func (w *Workspace) buildQueryMap() (map[string]*modconfig.Query, map[string]*modconfig.Query) { // build a list of long and short names for these queries var queryMap = make(map[string]*modconfig.Query) var localQueryMap = make(map[string]*modconfig.Query) @@ -76,7 +81,7 @@ func (w *Workspace) buildQueryMap(modMap modconfig.ModMap) (map[string]*modconfi } // for mod dependencies, add resources keyed by long name only - for _, mod := range modMap { + for _, mod := range w.Mods { for _, q := range mod.Queries { // if this mod is a direct dependency of the workspace mod, add it to the map _without_ a verison queryMap[q.Name()] = q @@ -86,7 +91,7 @@ func (w *Workspace) buildQueryMap(modMap modconfig.ModMap) (map[string]*modconfi return queryMap, localQueryMap } -func (w *Workspace) buildControlMap(modMap modconfig.ModMap) (map[string]*modconfig.Control, map[string]*modconfig.Control) { +func (w *Workspace) buildControlMap() (map[string]*modconfig.Control, map[string]*modconfig.Control) { // build a list of long and short names for these controls var controlMap = make(map[string]*modconfig.Control) var localControlMap = make(map[string]*modconfig.Control) @@ -97,7 +102,7 @@ func (w *Workspace) buildControlMap(modMap modconfig.ModMap) (map[string]*modcon } // for mod dependencies, add resources keyed by long name only - for _, mod := range modMap { + for _, mod := range w.Mods { for _, q := range mod.Controls { // if this mod is a direct dependency of the workspace mod, add it to the map _without_ a verison controlMap[q.Name()] = q @@ -107,7 +112,7 @@ func (w *Workspace) buildControlMap(modMap modconfig.ModMap) (map[string]*modcon return controlMap, localControlMap } -func (w *Workspace) buildBenchmarkMap(modMap modconfig.ModMap) (map[string]*modconfig.Benchmark, map[string]*modconfig.Benchmark) { +func (w *Workspace) buildBenchmarkMap() (map[string]*modconfig.Benchmark, map[string]*modconfig.Benchmark) { // build a list of long and short names for these benchmarks var benchmarkMap = make(map[string]*modconfig.Benchmark) var localBenchmarkMap = make(map[string]*modconfig.Benchmark) @@ -119,7 +124,7 @@ func (w *Workspace) buildBenchmarkMap(modMap modconfig.ModMap) (map[string]*modc } // for mod dependencies, add resources keyed by long name only - for _, mod := range modMap { + for _, mod := range w.Mods { for _, q := range mod.Benchmarks { // if this mod is a direct dependency of the workspace mod, add it to the map _without_ a verison benchmarkMap[q.Name()] = q @@ -129,14 +134,14 @@ func (w *Workspace) buildBenchmarkMap(modMap modconfig.ModMap) (map[string]*modc return benchmarkMap, localBenchmarkMap } -func (w *Workspace) buildDashboardMap(modMap modconfig.ModMap) map[string]*modconfig.Dashboard { +func (w *Workspace) buildDashboardMap() map[string]*modconfig.Dashboard { var res = make(map[string]*modconfig.Dashboard) for _, d := range w.Mod.Dashboards { res[d.Name()] = d } - for _, mod := range modMap { + for _, mod := range w.Mods { for _, d := range mod.Dashboards { res[d.Name()] = d } @@ -144,14 +149,14 @@ func (w *Workspace) buildDashboardMap(modMap modconfig.ModMap) map[string]*modco return res } -func (w *Workspace) buildDashboardContainerMap(modMap modconfig.ModMap) map[string]*modconfig.DashboardContainer { +func (w *Workspace) buildDashboardContainerMap() map[string]*modconfig.DashboardContainer { var res = make(map[string]*modconfig.DashboardContainer) for _, c := range w.Mod.DashboardContainers { res[c.Name()] = c } - for _, mod := range modMap { + for _, mod := range w.Mods { for _, c := range mod.DashboardContainers { res[c.Name()] = c } @@ -159,14 +164,14 @@ func (w *Workspace) buildDashboardContainerMap(modMap modconfig.ModMap) map[stri return res } -func (w *Workspace) buildDashboardCardMap(modMap modconfig.ModMap) map[string]*modconfig.DashboardCard { +func (w *Workspace) buildDashboardCardMap() map[string]*modconfig.DashboardCard { var res = make(map[string]*modconfig.DashboardCard) for _, p := range w.Mod.DashboardCards { res[p.Name()] = p } - for _, mod := range modMap { + for _, mod := range w.Mods { for _, p := range mod.DashboardCards { res[p.Name()] = p } @@ -174,14 +179,14 @@ func (w *Workspace) buildDashboardCardMap(modMap modconfig.ModMap) map[string]*m return res } -func (w *Workspace) buildDashboardChartMap(modMap modconfig.ModMap) map[string]*modconfig.DashboardChart { +func (w *Workspace) buildDashboardChartMap() map[string]*modconfig.DashboardChart { var res = make(map[string]*modconfig.DashboardChart) for _, c := range w.Mod.DashboardCharts { res[c.Name()] = c } - for _, mod := range modMap { + for _, mod := range w.Mods { for _, c := range mod.DashboardCharts { res[c.Name()] = c } @@ -189,14 +194,14 @@ func (w *Workspace) buildDashboardChartMap(modMap modconfig.ModMap) map[string]* return res } -func (w *Workspace) buildDashboardHierarchyMap(modMap modconfig.ModMap) map[string]*modconfig.DashboardHierarchy { +func (w *Workspace) buildDashboardHierarchyMap() map[string]*modconfig.DashboardHierarchy { var res = make(map[string]*modconfig.DashboardHierarchy) for _, p := range w.Mod.DashboardHierarchies { res[p.Name()] = p } - for _, mod := range modMap { + for _, mod := range w.Mods { for _, p := range mod.DashboardHierarchies { res[p.Name()] = p } @@ -204,14 +209,14 @@ func (w *Workspace) buildDashboardHierarchyMap(modMap modconfig.ModMap) map[stri return res } -func (w *Workspace) buildDashboardImageMap(modMap modconfig.ModMap) map[string]*modconfig.DashboardImage { +func (w *Workspace) buildDashboardImageMap() map[string]*modconfig.DashboardImage { var res = make(map[string]*modconfig.DashboardImage) for _, p := range w.Mod.DashboardImages { res[p.Name()] = p } - for _, mod := range modMap { + for _, mod := range w.Mods { for _, p := range mod.DashboardImages { res[p.Name()] = p } @@ -219,14 +224,14 @@ func (w *Workspace) buildDashboardImageMap(modMap modconfig.ModMap) map[string]* return res } -func (w *Workspace) buildDashboardInputMap(modMap modconfig.ModMap) map[string]*modconfig.DashboardInput { +func (w *Workspace) buildDashboardInputMap() map[string]*modconfig.DashboardInput { var res = make(map[string]*modconfig.DashboardInput) for _, p := range w.Mod.DashboardInputs { res[p.Name()] = p } - for _, mod := range modMap { + for _, mod := range w.Mods { for _, p := range mod.DashboardInputs { res[p.Name()] = p } @@ -234,14 +239,14 @@ func (w *Workspace) buildDashboardInputMap(modMap modconfig.ModMap) map[string]* return res } -func (w *Workspace) buildDashboardTableMap(modMap modconfig.ModMap) map[string]*modconfig.DashboardTable { +func (w *Workspace) buildDashboardTableMap() map[string]*modconfig.DashboardTable { var res = make(map[string]*modconfig.DashboardTable) for _, c := range w.Mod.DashboardTables { res[c.Name()] = c } - for _, mod := range modMap { + for _, mod := range w.Mods { for _, c := range mod.DashboardTables { res[c.Name()] = c } @@ -249,14 +254,14 @@ func (w *Workspace) buildDashboardTableMap(modMap modconfig.ModMap) map[string]* return res } -func (w *Workspace) buildDashboardTextMap(modMap modconfig.ModMap) map[string]*modconfig.DashboardText { +func (w *Workspace) buildDashboardTextMap() map[string]*modconfig.DashboardText { var res = make(map[string]*modconfig.DashboardText) for _, c := range w.Mod.DashboardTexts { res[c.Name()] = c } - for _, mod := range modMap { + for _, mod := range w.Mods { for _, c := range mod.DashboardTexts { res[c.Name()] = c } diff --git a/workspace/workspace_test.go b/workspace/workspace_test.go index 829baf5f..63aabc2f 100644 --- a/workspace/workspace_test.go +++ b/workspace/workspace_test.go @@ -12,6 +12,8 @@ import ( "github.com/turbot/steampipe/steampipeconfig/modconfig" ) +// TODO [reports] update this test - had to comment out queries + // the actual mod loading logic is tested more thoroughly in TestLoadMod (steampipeconfig/load_mod_test.go) // this test is primarily to verify the QueryMap building type loadWorkspaceTest struct { @@ -41,26 +43,26 @@ var testCasesLoadWorkspace = map[string]loadWorkspaceTest{ }, }, }, - Queries: map[string]*modconfig.Query{ - "w_1.query.localq1": { - ShortName: "localq1", Title: toStringPointer("LocalQ1"), Description: toStringPointer("THIS IS LOCAL QUERY 1"), SQL: toStringPointer(".tables"), - }, - "query.localq1": { - ShortName: "localq1", Title: toStringPointer("LocalQ1"), Description: toStringPointer("THIS IS LOCAL QUERY 1"), SQL: toStringPointer(".tables"), - }, - "w_2.query.localq2": { - ShortName: "localq2", Title: toStringPointer("LocalQ2"), Description: toStringPointer("THIS IS LOCAL QUERY 2"), SQL: toStringPointer(".inspect"), - }, - "query.localq2": { - ShortName: "localq2", Title: toStringPointer("LocalQ2"), Description: toStringPointer("THIS IS LOCAL QUERY 2"), SQL: toStringPointer(".inspect"), - }, - "m1.query.q1": { - ShortName: "q1", FullName: "Q1", Description: toStringPointer("THIS IS QUERY 1"), Documentation: toStringPointer("select 1"), - }, - "m2.query.q2": { - ShortName: "q2", FullName: "Q2", Description: toStringPointer("THIS IS QUERY 2"), Documentation: toStringPointer("select 2"), - }, - }, + //Queries: map[string]*modconfig.Query{ + // "w_1.query.localq1": { + // ShortName: "localq1", Title: toStringPointer("LocalQ1"), Description: toStringPointer("THIS IS LOCAL QUERY 1"), SQL: toStringPointer(".tables"), + // }, + // "query.localq1": { + // ShortName: "localq1", Title: toStringPointer("LocalQ1"), Description: toStringPointer("THIS IS LOCAL QUERY 1"), SQL: toStringPointer(".tables"), + // }, + // "w_2.query.localq2": { + // ShortName: "localq2", Title: toStringPointer("LocalQ2"), Description: toStringPointer("THIS IS LOCAL QUERY 2"), SQL: toStringPointer(".inspect"), + // }, + // "query.localq2": { + // ShortName: "localq2", Title: toStringPointer("LocalQ2"), Description: toStringPointer("THIS IS LOCAL QUERY 2"), SQL: toStringPointer(".inspect"), + // }, + // "m1.query.q1": { + // ShortName: "q1", FullName: "Q1", Description: toStringPointer("THIS IS QUERY 1"), Documentation: toStringPointer("select 1"), + // }, + // "m2.query.q2": { + // ShortName: "q2", FullName: "Q2", Description: toStringPointer("THIS IS QUERY 2"), Documentation: toStringPointer("select 2"), + // }, + //}, }, }, "single_mod_with_ignored_directory": { @@ -108,26 +110,26 @@ var testCasesLoadWorkspace = map[string]loadWorkspaceTest{ }, }, }, - Queries: map[string]*modconfig.Query{ - "w_1.query.localq1": { - ShortName: "localq1", Title: toStringPointer("LocalQ1"), Description: toStringPointer("THIS IS LOCAL QUERY 1"), SQL: toStringPointer(".tables"), - }, - "query.localq1": { - ShortName: "localq1", Title: toStringPointer("LocalQ1"), Description: toStringPointer("THIS IS LOCAL QUERY 1"), SQL: toStringPointer(".tables"), - }, - "w_2.query.localq2": { - ShortName: "localq2", Title: toStringPointer("LocalQ2"), Description: toStringPointer("THIS IS LOCAL QUERY 2"), SQL: toStringPointer(".inspect"), - }, - "query.localq2": { - ShortName: "localq2", Title: toStringPointer("LocalQ2"), Description: toStringPointer("THIS IS LOCAL QUERY 2"), SQL: toStringPointer(".inspect"), - }, - "m1.query.q1": { - ShortName: "q1", FullName: "Q1", Description: toStringPointer("THIS IS QUERY 1"), Documentation: toStringPointer("select 1"), - }, - "m2.query.q2": { - ShortName: "q2", FullName: "Q2", Description: toStringPointer("THIS IS QUERY 2"), Documentation: toStringPointer("select 2"), - }, - }, + //Queries: map[string]*modconfig.Query{ + // "w_1.query.localq1": { + // ShortName: "localq1", Title: toStringPointer("LocalQ1"), Description: toStringPointer("THIS IS LOCAL QUERY 1"), SQL: toStringPointer(".tables"), + // }, + // "query.localq1": { + // ShortName: "localq1", Title: toStringPointer("LocalQ1"), Description: toStringPointer("THIS IS LOCAL QUERY 1"), SQL: toStringPointer(".tables"), + // }, + // "w_2.query.localq2": { + // ShortName: "localq2", Title: toStringPointer("LocalQ2"), Description: toStringPointer("THIS IS LOCAL QUERY 2"), SQL: toStringPointer(".inspect"), + // }, + // "query.localq2": { + // ShortName: "localq2", Title: toStringPointer("LocalQ2"), Description: toStringPointer("THIS IS LOCAL QUERY 2"), SQL: toStringPointer(".inspect"), + // }, + // "m1.query.q1": { + // ShortName: "q1", FullName: "Q1", Description: toStringPointer("THIS IS QUERY 1"), Documentation: toStringPointer("select 1"), + // }, + // "m2.query.q2": { + // ShortName: "q2", FullName: "Q2", Description: toStringPointer("THIS IS QUERY 2"), Documentation: toStringPointer("select 2"), + // }, + //}, }, }, }