github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/workspace/workspace_queries.go (about) 1 package workspace 2 3 import ( 4 "fmt" 5 "log" 6 "os" 7 "path/filepath" 8 "strings" 9 10 "github.com/turbot/go-kit/helpers" 11 typehelpers "github.com/turbot/go-kit/types" 12 "github.com/turbot/steampipe/pkg/error_helpers" 13 "github.com/turbot/steampipe/pkg/steampipeconfig/modconfig" 14 "github.com/turbot/steampipe/pkg/steampipeconfig/parse" 15 "github.com/turbot/steampipe/pkg/utils" 16 ) 17 18 // GetQueriesFromArgs retrieves queries from args 19 // 20 // For each arg check if it is a named query or a file, before falling back to treating it as sql 21 func (w *Workspace) GetQueriesFromArgs(args []string) ([]*modconfig.ResolvedQuery, error) { 22 utils.LogTime("execute.GetQueriesFromArgs start") 23 defer utils.LogTime("execute.GetQueriesFromArgs end") 24 25 var queries = make([]*modconfig.ResolvedQuery, len(args)) 26 for idx, arg := range args { 27 resolvedQuery, queryProvider, err := w.ResolveQueryAndArgsFromSQLString(arg) 28 if err != nil { 29 return nil, err 30 } 31 if len(resolvedQuery.ExecuteSQL) > 0 { 32 // default name to the query text 33 resolvedQuery.Name = resolvedQuery.ExecuteSQL 34 if queryProvider != nil { 35 resolvedQuery.Name = queryProvider.Name() 36 } 37 queries[idx] = resolvedQuery 38 } 39 } 40 return queries, nil 41 } 42 43 // ResolveQueryAndArgsFromSQLString attempts to resolve 'arg' to a query and query args 44 func (w *Workspace) ResolveQueryAndArgsFromSQLString(sqlString string) (*modconfig.ResolvedQuery, modconfig.QueryProvider, error) { 45 var err error 46 47 // 1) check if this is a resource 48 // if this looks like a named query provider invocation, parse the sql string for arguments 49 resource, args, err := w.extractQueryProviderFromQueryString(sqlString) 50 if err != nil { 51 return nil, nil, err 52 } 53 54 if resource != nil { 55 log.Printf("[TRACE] query string is a query provider resource: %s", resource.Name()) 56 57 // resolve the query for the query provider and return it 58 resolvedQuery, err := w.ResolveQueryFromQueryProvider(resource, args) 59 if err != nil { 60 return nil, nil, err 61 } 62 log.Printf("[TRACE] resolved query: %s", sqlString) 63 return resolvedQuery, resource, nil 64 } 65 66 // 2) is this a file 67 // get absolute filename 68 filePath, err := filepath.Abs(sqlString) 69 if err != nil { 70 return nil, nil, fmt.Errorf("%s", err.Error()) 71 } 72 fileQuery, fileExists, err := w.getQueryFromFile(filePath) 73 if err != nil { 74 return nil, nil, fmt.Errorf("%s", err.Error()) 75 } 76 if fileExists { 77 if fileQuery.ExecuteSQL == "" { 78 error_helpers.ShowWarning(fmt.Sprintf("file '%s' does not contain any data", filePath)) 79 // (just return the empty query - it will be filtered above) 80 } 81 return fileQuery, nil, nil 82 } 83 // the argument cannot be resolved as an existing file 84 // if it has a sql suffix (i.e we believe the user meant to specify a file) return a file not found error 85 if strings.HasSuffix(strings.ToLower(sqlString), ".sql") { 86 return nil, nil, fmt.Errorf("file '%s' does not exist", filePath) 87 } 88 89 // so we have not managed to resolve this - if it looks like a named query or control 90 // (i.e we believe the user meant to specify a query) return a not found error 91 // NOTE: this needs to come after the file suffix check because this handles the resource name query.sql edge case 92 if name, isResource := queryLooksLikeExecutableResource(sqlString); isResource { 93 return nil, nil, fmt.Errorf("'%s' not found in %s (%s)", name, w.Mod.Name(), w.Path) 94 } 95 96 // 3) just use the query string as is and assume it is valid SQL 97 return &modconfig.ResolvedQuery{RawSQL: sqlString, ExecuteSQL: sqlString}, nil, nil 98 } 99 100 // ResolveQueryFromQueryProvider resolves the query for the given QueryProvider 101 func (w *Workspace) ResolveQueryFromQueryProvider(queryProvider modconfig.QueryProvider, runtimeArgs *modconfig.QueryArgs) (*modconfig.ResolvedQuery, error) { 102 log.Printf("[TRACE] ResolveQueryFromQueryProvider for %s", queryProvider.Name()) 103 104 query := queryProvider.GetQuery() 105 sql := queryProvider.GetSQL() 106 107 params := queryProvider.GetParams() 108 109 // merge the base args with the runtime args 110 var err error 111 runtimeArgs, err = modconfig.MergeArgs(queryProvider, runtimeArgs) 112 if err != nil { 113 return nil, err 114 } 115 116 // determine the source for the query 117 // - this will either be the control itself or any named query the control refers to 118 // either via its SQL proper ty (passing a query name) or Query property (using a reference to a query object) 119 120 // if a query is provided, use that to resolve the sql 121 if query != nil { 122 return w.ResolveQueryFromQueryProvider(query, runtimeArgs) 123 } 124 125 // must have sql is there is no query 126 if sql == nil { 127 return nil, fmt.Errorf("%s does not define either a 'sql' property or a 'query' property\n", queryProvider.Name()) 128 } 129 130 queryProviderSQL := typehelpers.SafeString(sql) 131 log.Printf("[TRACE] control defines inline SQL") 132 133 // if the SQL refers to a named query, this is the same as if the 'Query' property is set 134 if namedQueryProvider, ok := w.GetQueryProvider(queryProviderSQL); ok { 135 // in this case, it is NOT valid for the query provider to define its own Param definitions 136 if params != nil { 137 return nil, fmt.Errorf("%s has an 'SQL' property which refers to %s, so it cannot define 'param' blocks", queryProvider.Name(), namedQueryProvider.Name()) 138 } 139 return w.ResolveQueryFromQueryProvider(namedQueryProvider, runtimeArgs) 140 } 141 142 // so the sql is NOT a named query 143 return queryProvider.GetResolvedQuery(runtimeArgs) 144 145 } 146 147 // try to treat the input string as a file name and if it exists, return its contents 148 func (w *Workspace) getQueryFromFile(input string) (*modconfig.ResolvedQuery, bool, error) { 149 // get absolute filename 150 path, err := filepath.Abs(input) 151 if err != nil { 152 //nolint:golint,nilerr // if this gives any error, return not exist 153 return nil, false, nil 154 } 155 156 // does it exist? 157 if _, err := os.Stat(path); err != nil { 158 //nolint:golint,nilerr // if this gives any error, return not exist (we may get a not found or a path too long for example) 159 return nil, false, nil 160 } 161 162 // read file 163 fileBytes, err := os.ReadFile(path) 164 if err != nil { 165 return nil, true, err 166 } 167 168 res := &modconfig.ResolvedQuery{ 169 RawSQL: string(fileBytes), 170 ExecuteSQL: string(fileBytes), 171 } 172 return res, true, nil 173 } 174 175 // does the input look like a resource which can be executed as a query 176 // Note: if anything fails just return nil values 177 func (w *Workspace) extractQueryProviderFromQueryString(input string) (modconfig.QueryProvider, *modconfig.QueryArgs, error) { 178 // can we extract a resource name from the string 179 parsedResourceName := extractResourceNameFromQuery(input) 180 if parsedResourceName == nil { 181 return nil, nil, nil 182 } 183 // ok we managed to extract a resource name - does this resource exist? 184 resource, ok := w.GetResource(parsedResourceName) 185 if !ok { 186 return nil, nil, nil 187 } 188 189 //- is the resource a query provider, and if so does it have a query? 190 queryProvider, ok := resource.(modconfig.QueryProvider) 191 if !ok { 192 return nil, nil, fmt.Errorf("%s cannot be executed as a query", input) 193 } 194 195 _, args, err := parse.ParseQueryInvocation(input) 196 if err != nil { 197 return nil, nil, err 198 } 199 // success 200 return queryProvider, args, nil 201 } 202 203 func extractResourceNameFromQuery(input string) *modconfig.ParsedResourceName { 204 // remove parameters from the input string before calling ParseResourceName 205 // as parameters may break parsing 206 openBracketIdx := strings.Index(input, "(") 207 if openBracketIdx != -1 { 208 input = input[:openBracketIdx] 209 } 210 parsedName, err := modconfig.ParseResourceName(input) 211 // do not bubble error up, just return nil parsed name 212 // it is expected that this function may fail if a raw query is passed to it 213 if err != nil { 214 return nil 215 } 216 return parsedName 217 } 218 219 func queryLooksLikeExecutableResource(input string) (string, bool) { 220 // remove parameters from the input string before calling ParseResourceName 221 // as parameters may break parsing 222 openBracketIdx := strings.Index(input, "(") 223 if openBracketIdx != -1 { 224 input = input[:openBracketIdx] 225 } 226 parsedName, err := modconfig.ParseResourceName(input) 227 if err == nil && helpers.StringSliceContains(modconfig.QueryProviderBlocks, parsedName.ItemType) { 228 return parsedName.ToResourceName(), true 229 } 230 // do not bubble error up, just return false 231 return "", false 232 233 }