github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/control/controldisplay/group.go (about) 1 package controldisplay 2 3 import ( 4 "fmt" 5 "log" 6 "strings" 7 8 "github.com/turbot/steampipe/pkg/control/controlexecute" 9 "github.com/turbot/steampipe/pkg/steampipeconfig/modconfig" 10 ) 11 12 type GroupRenderer struct { 13 group *controlexecute.ResultGroup 14 // screen width 15 width int 16 maxFailedControls int 17 maxTotalControls int 18 resultTree *controlexecute.ExecutionTree 19 lastChild bool 20 parent *GroupRenderer 21 } 22 23 func NewGroupRenderer(group *controlexecute.ResultGroup, parent *GroupRenderer, maxFailedControls, maxTotalControls int, resultTree *controlexecute.ExecutionTree, width int) *GroupRenderer { 24 r := &GroupRenderer{ 25 group: group, 26 parent: parent, 27 resultTree: resultTree, 28 maxFailedControls: maxFailedControls, 29 maxTotalControls: maxTotalControls, 30 width: width, 31 } 32 r.lastChild = r.isLastChild(group) 33 return r 34 } 35 36 // are we the last child of our parent? 37 // this affects the tree rendering 38 func (r GroupRenderer) isLastChild(group *controlexecute.ResultGroup) bool { 39 if group.Parent == nil || group.Parent.GroupItem == nil { 40 return true 41 } 42 siblings := group.Parent.GroupItem.GetChildren() 43 // get the name of the last sibling which has controls (or is a control) 44 var finalSiblingName string 45 for _, s := range siblings { 46 if b, ok := s.(*modconfig.Benchmark); ok { 47 // find the result group for this benchmark and see if it has controls 48 resultGroup := r.resultTree.Root.GetChildGroupByName(b.Name()) 49 // if the result group has not controls, we will not find it in the result tree 50 if resultGroup == nil || resultGroup.ControlRunCount() == 0 { 51 continue 52 } 53 } 54 // store the name of this sibling 55 finalSiblingName = s.Name() 56 } 57 58 res := group.GroupItem.Name() == finalSiblingName 59 60 return res 61 } 62 63 // the indent for blank lines 64 // same as for (not last) children 65 func (r GroupRenderer) blankLineIndent() string { 66 return r.childIndent() 67 } 68 69 // the indent for group heading 70 func (r GroupRenderer) headingIndent() string { 71 // if this is the first displayed node, no indent 72 if r.parent == nil || r.parent.group.GroupId == controlexecute.RootResultGroupName { 73 return "" 74 } 75 // as our parent for the indent for a group 76 i := r.parent.childGroupIndent() 77 return i 78 } 79 80 // the indent for child groups/controls (which are not the final child) 81 // include the tree '|' 82 func (r GroupRenderer) childIndent() string { 83 return r.parentIndent() + "| " 84 } 85 86 // the indent for the FINAL child groups/controls 87 // just a space 88 func (r GroupRenderer) lastChildIndent() string { 89 return r.parentIndent() + " " 90 } 91 92 // the indent for child groups - our parent indent with the group expander "+ " 93 func (r GroupRenderer) childGroupIndent() string { 94 return r.parentIndent() + "+ " 95 } 96 97 // get the indent inherited from our parent 98 // - this will depend on whether we are our parents last child 99 func (r GroupRenderer) parentIndent() string { 100 if r.parent == nil || r.parent.group.GroupId == controlexecute.RootResultGroupName { 101 return "" 102 } 103 if r.lastChild { 104 return r.parent.lastChildIndent() 105 } 106 return r.parent.childIndent() 107 } 108 109 func (r GroupRenderer) Render() string { 110 if r.width <= 0 { 111 // this should never happen, since the minimum width is set by the formatter 112 log.Printf("[WARN] group renderer has width of %d\n", r.width) 113 return "" 114 } 115 116 if r.group.GroupId == controlexecute.RootResultGroupName { 117 return r.renderRootResultGroup() 118 } 119 120 groupHeadingRenderer := NewGroupHeadingRenderer( 121 r.group.Title, 122 r.group.Summary.Status.FailedCount(), 123 r.group.Summary.Status.TotalCount(), 124 r.maxFailedControls, 125 r.maxTotalControls, 126 r.width, 127 r.headingIndent()) 128 129 // render this group header 130 tableStrings := append([]string{}, 131 groupHeadingRenderer.Render(), 132 // newline after group 133 fmt.Sprintf("%s", ControlColors.Indent(r.blankLineIndent()))) 134 135 // now render the group children, in the order they are specified 136 childStrings := r.renderChildren() 137 tableStrings = append(tableStrings, childStrings...) 138 return strings.Join(tableStrings, "\n") 139 } 140 141 // for root result group, there will either be one or more groups, or one or more control runs 142 // there will be no order specified so just loop through them 143 func (r GroupRenderer) renderRootResultGroup() string { 144 var resultStrings = make([]string, len(r.group.Groups)+len(r.group.ControlRuns)) 145 for i, group := range r.group.Groups { 146 groupRenderer := NewGroupRenderer(group, &r, r.maxFailedControls, r.maxTotalControls, r.resultTree, r.width) 147 resultStrings[i] = groupRenderer.Render() 148 } 149 for i, run := range r.group.ControlRuns { 150 controlRenderer := NewControlRenderer(run, &r) 151 resultStrings[i] = controlRenderer.Render() 152 } 153 return strings.Join(resultStrings, "\n") 154 } 155 156 // render the children of this group, in the order they are specified in the hcl 157 func (r GroupRenderer) renderChildren() []string { 158 children := r.group.GroupItem.GetChildren() 159 var childStrings []string 160 161 for _, child := range children { 162 if control, ok := child.(*modconfig.Control); ok { 163 // get Result group with a matching name 164 if run := r.group.GetControlRunByName(control.Name()); run != nil { 165 controlRenderer := NewControlRenderer(run, &r) 166 childStrings = append(childStrings, controlRenderer.Render()) 167 } 168 } else { 169 if childGroup := r.group.GetGroupByName(child.Name()); childGroup != nil { 170 groupRenderer := NewGroupRenderer(childGroup, &r, r.maxFailedControls, r.maxTotalControls, r.resultTree, r.width) 171 childStrings = append(childStrings, groupRenderer.Render()) 172 } 173 } 174 } 175 176 return childStrings 177 }