github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/db/db_local/introspection_tables.go (about)

     1  package db_local
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"reflect"
     8  	"strings"
     9  
    10  	"github.com/jackc/pgx/v5"
    11  	"github.com/spf13/viper"
    12  	"github.com/turbot/go-kit/helpers"
    13  	typeHelpers "github.com/turbot/go-kit/types"
    14  	"github.com/turbot/pipe-fittings/hclhelpers"
    15  	"github.com/turbot/steampipe/pkg/constants"
    16  	"github.com/turbot/steampipe/pkg/db/db_common"
    17  	"github.com/turbot/steampipe/pkg/steampipeconfig/modconfig"
    18  	"github.com/turbot/steampipe/pkg/utils"
    19  	"github.com/zclconf/go-cty/cty"
    20  )
    21  
    22  // TagColumn is the tag used to specify the column name and type in the introspection tables
    23  const TagColumn = "column"
    24  
    25  func CreateIntrospectionTables(ctx context.Context, workspaceResources *modconfig.ResourceMaps, tx pgx.Tx) error {
    26  	// get the sql for columns which every table has
    27  	commonColumnSql := getColumnDefinitions(modconfig.ResourceMetadata{})
    28  
    29  	// convert to lowercase to avoid case sensitivity
    30  	switch strings.ToLower(viper.GetString(constants.ArgIntrospection)) {
    31  	case constants.IntrospectionInfo:
    32  		return populateAllIntrospectionTables(ctx, workspaceResources, tx, commonColumnSql)
    33  	case constants.IntrospectionControl:
    34  		return populateControlIntrospectionTables(ctx, workspaceResources, tx, commonColumnSql)
    35  	default:
    36  		return nil
    37  	}
    38  }
    39  
    40  func populateAllIntrospectionTables(ctx context.Context, workspaceResources *modconfig.ResourceMaps, tx pgx.Tx, commonColumnSql []string) error {
    41  	utils.LogTime("db.CreateIntrospectionTables start")
    42  	defer utils.LogTime("db.CreateIntrospectionTables end")
    43  
    44  	// get the create sql for each table type
    45  	createSql := getCreateTablesSql(commonColumnSql)
    46  
    47  	// now get sql to populate the tables
    48  	insertSql := getTableInsertSql(workspaceResources)
    49  	sql := []string{createSql, insertSql}
    50  
    51  	_, err := tx.Exec(ctx, strings.Join(sql, "\n"))
    52  	if err != nil {
    53  		return fmt.Errorf("failed to create introspection tables: %v", err)
    54  	}
    55  	// return context error - this enables calling code to respond to cancellation
    56  	return ctx.Err()
    57  }
    58  
    59  func populateControlIntrospectionTables(ctx context.Context, workspaceResources *modconfig.ResourceMaps, tx pgx.Tx, commonColumnSql []string) error {
    60  	utils.LogTime("db.CreateIntrospectionTables start")
    61  	defer utils.LogTime("db.CreateIntrospectionTables end")
    62  
    63  	// get the create sql for control and benchmark tables
    64  	createSql := getCreateControlTablesSql(commonColumnSql)
    65  	// now get sql to populate the control and benchmark tables
    66  	insertSql := getControlTableInsertSql(workspaceResources)
    67  	sql := []string{createSql, insertSql}
    68  
    69  	_, err := tx.Exec(ctx, strings.Join(sql, "\n"))
    70  	if err != nil {
    71  		return fmt.Errorf("failed to create introspection tables: %v", err)
    72  	}
    73  
    74  	// return context error - this enables calling code to respond to cancellation
    75  	return ctx.Err()
    76  }
    77  
    78  func getCreateTablesSql(commonColumnSql []string) string {
    79  	var createSql []string
    80  	createSql = append(createSql, getTableCreateSqlForResource(&modconfig.Control{}, constants.IntrospectionTableControl, commonColumnSql))
    81  	createSql = append(createSql, getTableCreateSqlForResource(&modconfig.Query{}, constants.IntrospectionTableQuery, commonColumnSql))
    82  	createSql = append(createSql, getTableCreateSqlForResource(&modconfig.Benchmark{}, constants.IntrospectionTableBenchmark, commonColumnSql))
    83  	createSql = append(createSql, getTableCreateSqlForResource(&modconfig.Mod{}, constants.IntrospectionTableMod, commonColumnSql))
    84  	createSql = append(createSql, getTableCreateSqlForResource(&modconfig.Variable{}, constants.IntrospectionTableVariable, commonColumnSql))
    85  	createSql = append(createSql, getTableCreateSqlForResource(&modconfig.Dashboard{}, constants.IntrospectionTableDashboard, commonColumnSql))
    86  	createSql = append(createSql, getTableCreateSqlForResource(&modconfig.DashboardContainer{}, constants.IntrospectionTableDashboardContainer, commonColumnSql))
    87  	createSql = append(createSql, getTableCreateSqlForResource(&modconfig.DashboardCard{}, constants.IntrospectionTableDashboardCard, commonColumnSql))
    88  	createSql = append(createSql, getTableCreateSqlForResource(&modconfig.DashboardChart{}, constants.IntrospectionTableDashboardChart, commonColumnSql))
    89  	createSql = append(createSql, getTableCreateSqlForResource(&modconfig.DashboardFlow{}, constants.IntrospectionTableDashboardFlow, commonColumnSql))
    90  	createSql = append(createSql, getTableCreateSqlForResource(&modconfig.DashboardGraph{}, constants.IntrospectionTableDashboardGraph, commonColumnSql))
    91  	createSql = append(createSql, getTableCreateSqlForResource(&modconfig.DashboardHierarchy{}, constants.IntrospectionTableDashboardHierarchy, commonColumnSql))
    92  	createSql = append(createSql, getTableCreateSqlForResource(&modconfig.DashboardImage{}, constants.IntrospectionTableDashboardImage, commonColumnSql))
    93  	createSql = append(createSql, getTableCreateSqlForResource(&modconfig.DashboardInput{}, constants.IntrospectionTableDashboardInput, commonColumnSql))
    94  	createSql = append(createSql, getTableCreateSqlForResource(&modconfig.DashboardTable{}, constants.IntrospectionTableDashboardTable, commonColumnSql))
    95  	createSql = append(createSql, getTableCreateSqlForResource(&modconfig.DashboardText{}, constants.IntrospectionTableDashboardText, commonColumnSql))
    96  	createSql = append(createSql, getTableCreateSqlForResource(&modconfig.ResourceReference{}, constants.IntrospectionTableReference, commonColumnSql))
    97  	return strings.Join(createSql, "\n")
    98  }
    99  
   100  func getTableInsertSql(workspaceResources *modconfig.ResourceMaps) string {
   101  	var insertSql []string
   102  
   103  	for _, control := range workspaceResources.Controls {
   104  		insertSql = append(insertSql, getTableInsertSqlForResource(control, constants.IntrospectionTableControl))
   105  	}
   106  	for _, query := range workspaceResources.Queries {
   107  		insertSql = append(insertSql, getTableInsertSqlForResource(query, constants.IntrospectionTableQuery))
   108  	}
   109  	for _, benchmark := range workspaceResources.Benchmarks {
   110  		insertSql = append(insertSql, getTableInsertSqlForResource(benchmark, constants.IntrospectionTableBenchmark))
   111  	}
   112  	for _, mod := range workspaceResources.Mods {
   113  		if !mod.IsDefaultMod() {
   114  			insertSql = append(insertSql, getTableInsertSqlForResource(mod, constants.IntrospectionTableMod))
   115  		}
   116  	}
   117  	for _, variable := range workspaceResources.Variables {
   118  		insertSql = append(insertSql, getTableInsertSqlForResource(variable, constants.IntrospectionTableVariable))
   119  	}
   120  	for _, dashboard := range workspaceResources.Dashboards {
   121  		insertSql = append(insertSql, getTableInsertSqlForResource(dashboard, constants.IntrospectionTableDashboard))
   122  	}
   123  	for _, container := range workspaceResources.DashboardContainers {
   124  		insertSql = append(insertSql, getTableInsertSqlForResource(container, constants.IntrospectionTableDashboardContainer))
   125  	}
   126  	for _, card := range workspaceResources.DashboardCards {
   127  		insertSql = append(insertSql, getTableInsertSqlForResource(card, constants.IntrospectionTableDashboardCard))
   128  	}
   129  	for _, chart := range workspaceResources.DashboardCharts {
   130  		insertSql = append(insertSql, getTableInsertSqlForResource(chart, constants.IntrospectionTableDashboardChart))
   131  	}
   132  	for _, flow := range workspaceResources.DashboardFlows {
   133  		insertSql = append(insertSql, getTableInsertSqlForResource(flow, constants.IntrospectionTableDashboardFlow))
   134  	}
   135  	for _, graph := range workspaceResources.DashboardGraphs {
   136  		insertSql = append(insertSql, getTableInsertSqlForResource(graph, constants.IntrospectionTableDashboardGraph))
   137  	}
   138  	for _, hierarchy := range workspaceResources.DashboardHierarchies {
   139  		insertSql = append(insertSql, getTableInsertSqlForResource(hierarchy, constants.IntrospectionTableDashboardHierarchy))
   140  	}
   141  	for _, image := range workspaceResources.DashboardImages {
   142  		insertSql = append(insertSql, getTableInsertSqlForResource(image, constants.IntrospectionTableDashboardImage))
   143  	}
   144  	for _, dashboardInputs := range workspaceResources.DashboardInputs {
   145  		for _, input := range dashboardInputs {
   146  			insertSql = append(insertSql, getTableInsertSqlForResource(input, constants.IntrospectionTableDashboardInput))
   147  		}
   148  	}
   149  	for _, input := range workspaceResources.GlobalDashboardInputs {
   150  		insertSql = append(insertSql, getTableInsertSqlForResource(input, constants.IntrospectionTableDashboardInput))
   151  	}
   152  	for _, table := range workspaceResources.DashboardTables {
   153  		insertSql = append(insertSql, getTableInsertSqlForResource(table, constants.IntrospectionTableDashboardTable))
   154  	}
   155  	for _, text := range workspaceResources.DashboardTexts {
   156  		insertSql = append(insertSql, getTableInsertSqlForResource(text, constants.IntrospectionTableDashboardText))
   157  	}
   158  	for _, reference := range workspaceResources.References {
   159  		insertSql = append(insertSql, getTableInsertSqlForResource(reference, constants.IntrospectionTableReference))
   160  	}
   161  
   162  	return strings.Join(insertSql, "\n")
   163  }
   164  
   165  // reflect on the `column` tag for this given resource and any nested structs
   166  // to build the introspection table creation sql
   167  // NOTE: ensure the object passed to this is a pointer, as otherwise the interface type casts will return false
   168  func getTableCreateSqlForResource(s interface{}, tableName string, commonColumnSql []string) string {
   169  	columnDefinitions := append(commonColumnSql, getColumnDefinitions(s)...)
   170  	if qp, ok := s.(modconfig.QueryProvider); ok {
   171  		columnDefinitions = append(columnDefinitions, getColumnDefinitions(qp.GetQueryProviderImpl())...)
   172  	}
   173  	if mti, ok := s.(modconfig.ModTreeItem); ok {
   174  		columnDefinitions = append(columnDefinitions, getColumnDefinitions(mti.GetModTreeItemImpl())...)
   175  	}
   176  	if hr, ok := s.(modconfig.HclResource); ok {
   177  		columnDefinitions = append(columnDefinitions, getColumnDefinitions(hr.GetHclResourceImpl())...)
   178  	}
   179  
   180  	// Query cannot define 'query' as a property.
   181  	// So for a steampipe_query table, we will exclude the query column.
   182  	// Here we are removing the column named query from the 'columnDefinitions' slice.
   183  	if tableName == "steampipe_query" {
   184  		// find the index of the element 'query' and store in idx
   185  		for i, col := range columnDefinitions {
   186  			if col == "  query  text" {
   187  				// remove the idx element from 'columnDefinitions' slice
   188  				columnDefinitions = utils.RemoveElementFromSlice(columnDefinitions, i)
   189  				break
   190  			}
   191  		}
   192  
   193  	}
   194  
   195  	tableSql := fmt.Sprintf(`create temp table %s (
   196  %s
   197  );`, tableName, strings.Join(columnDefinitions, ",\n"))
   198  	return tableSql
   199  }
   200  
   201  func getCreateControlTablesSql(commonColumnSql []string) string {
   202  	var createSql []string
   203  	createSql = append(createSql, getTableCreateSqlForResource(&modconfig.Control{}, constants.IntrospectionTableControl, commonColumnSql))
   204  	createSql = append(createSql, getTableCreateSqlForResource(&modconfig.Benchmark{}, constants.IntrospectionTableBenchmark, commonColumnSql))
   205  	return strings.Join(createSql, "\n")
   206  }
   207  
   208  func getControlTableInsertSql(workspaceResources *modconfig.ResourceMaps) string {
   209  	var insertSql []string
   210  
   211  	for _, control := range workspaceResources.Controls {
   212  		insertSql = append(insertSql, getTableInsertSqlForResource(control, constants.IntrospectionTableControl))
   213  	}
   214  	for _, benchmark := range workspaceResources.Benchmarks {
   215  		insertSql = append(insertSql, getTableInsertSqlForResource(benchmark, constants.IntrospectionTableBenchmark))
   216  	}
   217  
   218  	return strings.Join(insertSql, "\n")
   219  }
   220  
   221  // getColumnDefinitions returns the sql column definitions for tagged properties of the item
   222  func getColumnDefinitions(item interface{}) []string {
   223  	t := reflect.TypeOf(item)
   224  	if t.Kind() == reflect.Pointer {
   225  		t = t.Elem()
   226  	}
   227  	var columnDef []string
   228  	val := reflect.ValueOf(item)
   229  	if val.Kind() == reflect.Pointer {
   230  		val = val.Elem()
   231  	}
   232  	for i := 0; i < val.NumField(); i++ {
   233  		fieldName := val.Type().Field(i).Name
   234  		field, _ := t.FieldByName(fieldName)
   235  		columnTag, ok := newColumnTag(field)
   236  		if !ok {
   237  			continue
   238  		}
   239  		columnDef = append(columnDef, fmt.Sprintf("  %s  %s", columnTag.Column, columnTag.ColumnType))
   240  	}
   241  	return columnDef
   242  }
   243  
   244  func getTableInsertSqlForResource(item any, tableName string) string {
   245  	// for each item there is core reflection data (i.e. reflection resource all items have)
   246  	// and item specific reflection data
   247  	// get the core reflection data values
   248  	var valuesCore, columnsCore []string
   249  	if rwm, ok := item.(modconfig.ResourceWithMetadata); ok {
   250  		valuesCore, columnsCore = getColumnValues(rwm.GetMetadata())
   251  	}
   252  
   253  	// get item specific reflection data values from the item
   254  	valuesItem, columnsItem := getColumnValues(item)
   255  	columns := append(columnsCore, columnsItem...)
   256  	values := append(valuesCore, valuesItem...)
   257  
   258  	// get properties from embedded structs
   259  	if qp, ok := item.(modconfig.QueryProvider); ok {
   260  		valuesItem, columnsItem = getColumnValues(qp.GetQueryProviderImpl())
   261  		columns = append(columns, columnsItem...)
   262  		values = append(values, valuesItem...)
   263  	}
   264  	if mti, ok := item.(modconfig.ModTreeItem); ok {
   265  		valuesItem, columnsItem = getColumnValues(mti.GetModTreeItemImpl())
   266  		columns = append(columns, columnsItem...)
   267  		values = append(values, valuesItem...)
   268  	}
   269  	if hr, ok := item.(modconfig.HclResource); ok {
   270  		valuesItem, columnsItem = getColumnValues(hr.GetHclResourceImpl())
   271  		columns = append(columns, columnsItem...)
   272  		values = append(values, valuesItem...)
   273  	}
   274  
   275  	insertSql := fmt.Sprintf(`insert into %s (%s) values(%s);`, tableName, strings.Join(columns, ","), strings.Join(values, ","))
   276  	return insertSql
   277  }
   278  
   279  // use reflection to evaluate the column names and values from item - return as 2 separate arrays
   280  func getColumnValues(item interface{}) ([]string, []string) {
   281  	if item == nil {
   282  		return nil, nil
   283  	}
   284  	var columns, values []string
   285  
   286  	// dereference item in vcase it is a pointer
   287  	item = helpers.DereferencePointer(item)
   288  
   289  	val := reflect.ValueOf(helpers.DereferencePointer(item))
   290  	t := reflect.TypeOf(item)
   291  
   292  	for i := 0; i < val.NumField(); i++ {
   293  		fieldName := val.Type().Field(i).Name
   294  		field, _ := t.FieldByName(fieldName)
   295  
   296  		columnTag, ok := newColumnTag(field)
   297  		if !ok {
   298  			continue
   299  		}
   300  
   301  		value, ok := helpers.GetFieldValueFromInterface(item, fieldName)
   302  
   303  		// all fields will be pointers
   304  		value = helpers.DereferencePointer(value)
   305  		if !ok || value == nil {
   306  			continue
   307  		}
   308  
   309  		// formatIntrospectionTableValue escapes values, and for json columns, converts them into escaped JSON
   310  		// ignore JSON conversion errors - trust that array values read from hcl will be convertable
   311  		formattedValue, _ := formatIntrospectionTableValue(value, columnTag)
   312  		values = append(values, formattedValue)
   313  		columns = append(columns, columnTag.Column)
   314  	}
   315  	return values, columns
   316  }
   317  
   318  // convert the value into a postgres format value which can used in an insert statement
   319  func formatIntrospectionTableValue(item interface{}, columnTag *ColumnTag) (string, error) {
   320  	// special handling for cty.Type and cty.Value data
   321  	switch t := item.(type) {
   322  	// if the item is a cty value, we always represent it as json
   323  	case cty.Value:
   324  		if columnTag.ColumnType != "jsonb" {
   325  			return "nil", fmt.Errorf("data for column %s is of type cty.Value so column type should be 'jsonb' but is actually %s", columnTag.Column, columnTag.ColumnType)
   326  		}
   327  		str, err := hclhelpers.CtyToJSON(t)
   328  		if err != nil {
   329  			return "", err
   330  		}
   331  		return db_common.PgEscapeString(str), nil
   332  	case cty.Type:
   333  		// if the item is a cty value, we always represent it as json
   334  		if columnTag.ColumnType != "text" {
   335  			return "nil", fmt.Errorf("data for column %s is of type cty.Type so column type should be 'text' but is actually %s", columnTag.Column, columnTag.ColumnType)
   336  		}
   337  		return db_common.PgEscapeString(t.FriendlyName()), nil
   338  	}
   339  
   340  	switch columnTag.ColumnType {
   341  	case "jsonb":
   342  		jsonBytes, err := json.Marshal(reflect.ValueOf(item).Interface())
   343  		if err != nil {
   344  			return "", err
   345  		}
   346  
   347  		res := db_common.PgEscapeString(string(jsonBytes))
   348  		return res, nil
   349  	case "integer", "numeric", "decimal", "boolean":
   350  		return typeHelpers.ToString(item), nil
   351  	default:
   352  		// for string column, escape the data
   353  		return db_common.PgEscapeString(typeHelpers.ToString(item)), nil
   354  	}
   355  }