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  }