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 }