github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/interactive/metaquery/handler_inspect.go (about) 1 package metaquery 2 3 import ( 4 "context" 5 "fmt" 6 "log" 7 "regexp" 8 "sort" 9 "strings" 10 "time" 11 12 "github.com/turbot/steampipe-plugin-sdk/v5/sperr" 13 "github.com/turbot/steampipe/pkg/constants" 14 "github.com/turbot/steampipe/pkg/display" 15 "github.com/turbot/steampipe/pkg/error_helpers" 16 "github.com/turbot/steampipe/pkg/steampipeconfig" 17 ) 18 19 // inspect 20 func inspect(ctx context.Context, input *HandlerInput) error { 21 connStateMap, err := input.GetConnectionStateMap(ctx) 22 if err != nil { 23 return err 24 } 25 if connStateMap == nil { 26 log.Printf("[TRACE] failed to load connection state - are we connected to a server running a previous steampipe version?") 27 // if there is no connection state, call legacy inspect 28 return inspectLegacy(ctx, input) 29 } 30 31 // if no args were provided just list connections 32 if len(input.args()) == 0 { 33 return listConnections(ctx, input) 34 } 35 36 // so there were args, try to determine what the args are 37 tableOrConnection := input.args()[0] 38 if len(input.args()) > 0 { 39 // this should be one argument, but may have been split by the tokenizer 40 // because of the escape characters that autocomplete puts in 41 // join them up 42 tableOrConnection = strings.Join(input.args(), " ") 43 } 44 45 // remove all double quotes (if any) 46 tableOrConnection = strings.Join( 47 strings.Split(tableOrConnection, "\""), 48 "", 49 ) 50 51 // arg can be one of <connection_name> or <connection_name>.<table_name> 52 tokens := strings.SplitN(tableOrConnection, ".", 2) 53 54 // here tokens could be schema.tableName or tableName 55 56 if len(tokens) == 1 { 57 // only a connection name (or maybe unqualified table name) 58 return inspectSchemaOrUnqualifiedTable(ctx, tableOrConnection, input) 59 } 60 61 // this is a fully qualified table name 62 return inspectQualifiedTable(ctx, tokens[0], tokens[1], input) 63 } 64 65 func inspectSchemaOrUnqualifiedTable(ctx context.Context, tableOrConnection string, input *HandlerInput) error { 66 // only a connection name (or maybe unqualified table name) 67 if inspectConnection(ctx, tableOrConnection, input) { 68 return nil 69 } 70 71 // there was no schema 72 // add the temporary schema to the search_path so that it becomes searchable 73 // for the next step 74 //nolint:golint,gocritic // we don't want to modify the input value 75 searchPath := append(input.SearchPath, input.Schema.TemporarySchemaName) 76 77 // go through the searchPath one by one and try to find the table by this name 78 for _, schema := range searchPath { 79 tablesInThisSchema := input.Schema.GetTablesInSchema(schema) 80 // we have a table by this name here 81 if _, gotTable := tablesInThisSchema[tableOrConnection]; gotTable { 82 return inspectQualifiedTable(ctx, schema, tableOrConnection, input) 83 } 84 85 // check against the fully qualified name of the table 86 for _, table := range input.Schema.Schemas[schema] { 87 if tableOrConnection == table.FullName { 88 return inspectQualifiedTable(ctx, schema, table.Name, input) 89 } 90 } 91 } 92 93 return fmt.Errorf("could not find connection or table called '%s'. Is the plugin installed? Is the connection configured?", tableOrConnection) 94 } 95 96 // list all the tables in the schema 97 func listTables(ctx context.Context, input *HandlerInput) error { 98 99 if len(input.args()) == 0 { 100 schemas := input.Schema.GetSchemas() 101 for _, schema := range schemas { 102 if schema == input.Schema.TemporarySchemaName { 103 continue 104 } 105 fmt.Printf(" ==> %s\n", schema) 106 inspectConnection(ctx, schema, input) 107 } 108 109 fmt.Printf(` 110 To get information about the columns in a table, run %s 111 112 `, constants.Bold(".inspect {connection}.{table}")) 113 } else { 114 // could be one of connectionName and {string}* 115 arg := input.args()[0] 116 if !strings.HasSuffix(arg, "*") { 117 inspectConnection(ctx, arg, input) 118 fmt.Println() 119 return nil 120 } 121 122 // treat this as a wild card 123 r, err := regexp.Compile(arg) 124 if err != nil { 125 return fmt.Errorf("invalid search string %s", arg) 126 } 127 header := []string{"Table", "Schema"} 128 var rows [][]string 129 for schemaName, schemaDetails := range input.Schema.Schemas { 130 var tables [][]string 131 for tableName := range schemaDetails { 132 if r.MatchString(tableName) { 133 tables = append(tables, []string{tableName, schemaName}) 134 } 135 } 136 sort.SliceStable(tables, func(i, j int) bool { 137 return tables[i][0] < tables[j][0] 138 }) 139 rows = append(rows, tables...) 140 } 141 display.ShowWrappedTable(header, rows, &display.ShowWrappedTableOptions{AutoMerge: true}) 142 } 143 144 return nil 145 } 146 147 func listConnections(ctx context.Context, input *HandlerInput) error { 148 connStateMap, err := input.GetConnectionStateMap(ctx) 149 if err != nil { 150 return err 151 } 152 // if there is no connection state in the input, call listConnectionsLegacy 153 if connStateMap == nil { 154 log.Printf("[TRACE] failed to load connection state - are we connected to a server running a previous steampipe version?") 155 // call legacy inspect 156 return listConnectionsLegacy(ctx, input) 157 } 158 159 header := []string{"connection", "plugin", "state"} 160 161 connectionState, err := input.GetConnectionStateMap(ctx) 162 if err != nil { 163 return err 164 } 165 showStateSummary := connectionState.ConnectionsInState( 166 constants.ConnectionStateUpdating, 167 constants.ConnectionStateDeleting, 168 constants.ConnectionStateError) 169 170 var rows [][]string 171 172 for connectionName, state := range connectionState { 173 // skip disabled connections 174 if state.Disabled() { 175 continue 176 } 177 row := []string{connectionName, state.Plugin, state.State} 178 rows = append(rows, row) 179 } 180 181 // sort by connection name 182 sort.SliceStable(rows, func(i, j int) bool { 183 return rows[i][0] < rows[j][0] 184 }) 185 186 display.ShowWrappedTable(header, rows, &display.ShowWrappedTableOptions{AutoMerge: false}) 187 188 if showStateSummary { 189 showStateSummaryTable(connectionState) 190 } 191 192 fmt.Printf(` 193 To get information about the tables in a connection, run %s 194 To get information about the columns in a table, run %s 195 196 `, constants.Bold(".inspect {connection}"), constants.Bold(".inspect {connection}.{table}")) 197 198 return nil 199 } 200 201 func showStateSummaryTable(connectionState steampipeconfig.ConnectionStateMap) { 202 header := []string{"Connection state", "Count"} 203 var rows [][]string 204 stateSummary := connectionState.GetSummary() 205 206 for _, state := range constants.ConnectionStates { 207 if connectionsInState := stateSummary[state]; connectionsInState > 0 { 208 rows = append(rows, []string{state, fmt.Sprintf("%d", connectionsInState)}) 209 } 210 } 211 display.ShowWrappedTable(header, rows, &display.ShowWrappedTableOptions{AutoMerge: false}) 212 } 213 214 func inspectQualifiedTable(ctx context.Context, connectionName string, tableName string, input *HandlerInput) error { 215 header := []string{"column", "type", "description"} 216 var rows [][]string 217 218 connectionStateMap, err := input.GetConnectionStateMap(ctx) 219 if err != nil { 220 return err 221 } 222 // do we have connection state for this schema and if so is it disabled? 223 if connectionState := connectionStateMap[connectionName]; connectionState != nil && connectionState.Disabled() { 224 error_helpers.ShowWarning(fmt.Sprintf("connection '%s' has schema import disabled", connectionName)) 225 return nil 226 } 227 228 schema, found := input.Schema.Schemas[connectionName] 229 if !found { 230 return fmt.Errorf("could not find connection called '%s'. Is the plugin installed? Is the connection configured?\n", connectionName) 231 } 232 tableSchema, found := schema[tableName] 233 if !found { 234 return fmt.Errorf("could not find table '%s' in '%s'", tableName, connectionName) 235 } 236 237 for _, columnSchema := range tableSchema.Columns { 238 rows = append(rows, []string{columnSchema.Name, columnSchema.Type, columnSchema.Description}) 239 } 240 241 // sort by column name 242 sort.SliceStable(rows, func(i, j int) bool { 243 return rows[i][0] < rows[j][0] 244 }) 245 246 display.ShowWrappedTable(header, rows, &display.ShowWrappedTableOptions{AutoMerge: false}) 247 248 return nil 249 } 250 251 // inspect the connection with the given name 252 // return whether connectionName was identified as an existing connection 253 func inspectConnection(ctx context.Context, connectionName string, input *HandlerInput) bool { 254 connectionStateMap, err := input.GetConnectionStateMap(ctx) 255 if err != nil { 256 error_helpers.ShowError(ctx, sperr.WrapWithMessage(err, "connection '%s' has schema import disabled", connectionName)) 257 return true 258 } 259 260 connectionState, connectionFoundInState := connectionStateMap[connectionName] 261 if !connectionFoundInState { 262 return false 263 } 264 if connectionState.Disabled() { 265 error_helpers.ShowWarning(fmt.Sprintf("connection '%s' has schema import disabled", connectionName)) 266 return true 267 } 268 269 // have we loaded the schema for this connection yet? 270 schema, found := input.Schema.Schemas[connectionName] 271 272 var rows [][]string 273 var header []string 274 275 if found { 276 header = []string{"table", "description"} 277 for _, tableSchema := range schema { 278 rows = append(rows, []string{tableSchema.Name, tableSchema.Description}) 279 } 280 } else { 281 // just display the connection state 282 header = []string{"connection", "plugin", "schema mode", "state", "error", "state updated"} 283 284 rows = [][]string{{ 285 connectionName, 286 connectionState.Plugin, 287 connectionState.SchemaMode, 288 connectionState.State, 289 connectionState.Error(), 290 connectionState.ConnectionModTime.Format(time.RFC3339), 291 }, 292 } 293 } 294 295 // sort by table name 296 sort.SliceStable(rows, func(i, j int) bool { 297 return rows[i][0] < rows[j][0] 298 }) 299 300 display.ShowWrappedTable(header, rows, &display.ShowWrappedTableOptions{AutoMerge: false}) 301 302 return true 303 }