github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/control/controlexecute/result_row.go (about) 1 package controlexecute 2 3 import ( 4 "fmt" 5 6 "github.com/turbot/go-kit/helpers" 7 typehelpers "github.com/turbot/go-kit/types" 8 "github.com/turbot/steampipe/pkg/constants" 9 "github.com/turbot/steampipe/pkg/dashboard/dashboardtypes" 10 "github.com/turbot/steampipe/pkg/query/queryresult" 11 "github.com/turbot/steampipe/pkg/steampipeconfig/modconfig" 12 "github.com/turbot/steampipe/pkg/utils" 13 ) 14 15 type ResultRows []*ResultRow 16 17 // ToLeafData converts the result rows to snapshot data format 18 func (r ResultRows) ToLeafData(dimensionSchema map[string]*queryresult.ColumnDef) *dashboardtypes.LeafData { 19 var res = &dashboardtypes.LeafData{ 20 Columns: []*queryresult.ColumnDef{ 21 {Name: "reason", DataType: "TEXT"}, 22 {Name: "resource", DataType: "TEXT"}, 23 {Name: "status", DataType: "TEXT"}, 24 }, 25 Rows: make([]map[string]interface{}, len(r)), 26 } 27 for _, d := range dimensionSchema { 28 res.Columns = append(res.Columns, d) 29 } 30 for i, row := range r { 31 res.Rows[i] = map[string]interface{}{ 32 "reason": row.Reason, 33 "resource": row.Resource, 34 "status": row.Status, 35 } 36 // flatten dimensions 37 for _, d := range row.Dimensions { 38 res.Rows[i][d.Key] = d.Value 39 } 40 } 41 return res 42 } 43 44 // ResultRow is the result of a control execution for a single resource 45 type ResultRow struct { 46 // reason for the status 47 Reason string `json:"reason" csv:"reason"` 48 // resource name 49 Resource string `json:"resource" csv:"resource"` 50 // status of the row (ok, info, alarm, error, skip) 51 Status string `json:"status" csv:"status"` 52 // dimensions for this row 53 Dimensions []Dimension `json:"dimensions"` 54 // parent control run 55 Run *ControlRun `json:"-"` 56 // source control 57 Control *modconfig.Control `json:"-" csv:"control_id:UnqualifiedName,control_title:Title,control_description:Description"` 58 } 59 60 // GetDimensionValue returns the value for a dimension key. Returns an empty string with 'false' if not found 61 func (r *ResultRow) GetDimensionValue(key string) string { 62 for _, dim := range r.Dimensions { 63 if dim.Key == key { 64 return dim.Value 65 } 66 } 67 return "" 68 } 69 70 // AddDimension checks whether a column value is a scalar type, and if so adds it to the Dimensions map 71 func (r *ResultRow) AddDimension(c *queryresult.ColumnDef, val interface{}) { 72 r.Dimensions = append(r.Dimensions, Dimension{ 73 Key: c.Name, 74 Value: typehelpers.ToString(val), 75 SqlType: c.DataType, 76 }) 77 } 78 79 func NewResultRow(run *ControlRun, row *queryresult.RowResult, cols []*queryresult.ColumnDef) (*ResultRow, error) { 80 // validate the required columns exist in the result 81 if err := validateColumns(cols); err != nil { 82 return nil, err 83 } 84 res := &ResultRow{ 85 Run: run, 86 Control: run.Control, 87 } 88 89 // was there a SQL error _executing the control 90 // Note: this is different from the control state being 'error' 91 if row.Error != nil { 92 return nil, row.Error 93 } 94 95 for i, c := range cols { 96 switch c.Name { 97 case "reason": 98 res.Reason = typehelpers.ToString(row.Data[i]) 99 case "resource": 100 res.Resource = typehelpers.ToString(row.Data[i]) 101 case "status": 102 status := typehelpers.ToString(row.Data[i]) 103 if !IsValidControlStatus(status) { 104 return nil, fmt.Errorf("invalid control status '%s'", status) 105 } 106 res.Status = status 107 default: 108 // if this is a scalar type, add to dimensions 109 val := row.Data[i] 110 // isScalar may mutate the ColumnDef struct by lazily populating the internal isScalar property 111 if c.IsScalar(val) { 112 res.AddDimension(c, val) 113 } 114 } 115 } 116 return res, nil 117 } 118 119 func IsValidControlStatus(status string) bool { 120 return helpers.StringSliceContains([]string{constants.ControlOk, constants.ControlAlarm, constants.ControlInfo, constants.ControlError, constants.ControlSkip}, status) 121 } 122 123 func validateColumns(cols []*queryresult.ColumnDef) error { 124 requiredColumns := []string{"reason", "resource", "status"} 125 var missingColumns []string 126 for _, col := range requiredColumns { 127 if !columnTypesContainsColumn(col, cols) { 128 missingColumns = append(missingColumns, col) 129 } 130 } 131 if len(missingColumns) > 0 { 132 return fmt.Errorf("control result is missing required %s: %v", utils.Pluralize("column", len(missingColumns)), missingColumns) 133 } 134 return nil 135 } 136 137 func columnTypesContainsColumn(col string, colTypes []*queryresult.ColumnDef) bool { 138 for _, ct := range colTypes { 139 if ct.Name == col { 140 return true 141 } 142 } 143 return false 144 }