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  }