Refactor prepared statement creation code to support all QueryProviders. Closes #1468

This commit is contained in:
kaidaguerre authored and GitHub committed 2022-02-17 14:41:23 +00:00
1 parent ff7fc944e9
commit 2c7d9950ed
44 files changed
+1091 -852

No files matched your search

+5 -114
View File
@@ -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() {
+39 -12
View File
@@ -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
}
+36 -36
View File
@@ -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
+11
View File
@@ -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"
+1 -1
View File
@@ -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())
}
+115
View File
@@ -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
}
+4 -4
View File
@@ -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
}
+3 -3
View File
@@ -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
@@ -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"`
@@ -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)
}
@@ -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 {
+1 -6
View File
@@ -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
+15 -10
View File
@@ -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
}
@@ -10,8 +10,8 @@ type DashboardRunStatus string
const (
DashboardRunReady DashboardRunStatus = "ready"
DashboardRunComplete = "complete"
DashboardRunError = "error"
DashboardRunComplete = "complete"
DashboardRunError = "error"
)
type DashboardNodeRun interface {
+12 -34
View File
@@ -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())
+85
View File
@@ -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
}
+1 -1
View File
@@ -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))
+19 -27
View File
@@ -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
}
-5
View File
@@ -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 {
+1 -1
View File
@@ -5,7 +5,7 @@ const (
BlockTypeQuery = "query"
BlockTypeControl = "control"
BlockTypeBenchmark = "benchmark"
BlockTypeDashboard = "dashboard"
BlockTypeDashboard = "dashboard"
BlockTypeContainer = "container"
BlockTypeChart = "chart"
BlockTypeCard = "card"
+22 -29
View File
@@ -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 {
+12 -6
View File
@@ -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
}
+23 -30
View File
@@ -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)
}
+22 -18
View File
@@ -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)
}
@@ -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 {
@@ -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)
}
+117 -73
View File
@@ -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.<shortName>'
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)
}
+113 -57
View File
@@ -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.<shortName>'
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)
}
+21 -17
View File
@@ -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)
}
@@ -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 {
+8 -6
View File
@@ -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
}
+1 -1
View File
@@ -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() {
@@ -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
}
+71 -65
View File
@@ -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)
@@ -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
}
@@ -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
}
}
}
+14 -12
View File
@@ -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())
@@ -16,6 +16,10 @@ dashboard "r1"{
}
}
control {
query = query.q1
}
}
//dashboard "r2"{
+6 -1
View File
@@ -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
}
+5 -58
View File
@@ -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
}
+2 -2
View File
@@ -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
+42 -37
View File
@@ -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)
+49 -44
View File
@@ -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
}
+42 -40
View File
@@ -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"),
// },
//},
},
},
}