github.com/SamarSidharth/kpt@v0.0.0-20231122062228-c7d747ae3ace/internal/alpha/printers/table/printer.go (about) 1 // Copyright 2020 The Kubernetes Authors. 2 // SPDX-License-Identifier: Apache-2.0 3 4 package table 5 6 import ( 7 "fmt" 8 "io" 9 "strings" 10 "time" 11 12 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 13 "k8s.io/cli-runtime/pkg/genericclioptions" 14 "k8s.io/klog/v2" 15 "sigs.k8s.io/cli-utils/pkg/apply/event" 16 "sigs.k8s.io/cli-utils/pkg/common" 17 pollingevent "sigs.k8s.io/cli-utils/pkg/kstatus/polling/event" 18 "sigs.k8s.io/cli-utils/pkg/kstatus/status" 19 printcommon "sigs.k8s.io/cli-utils/pkg/print/common" 20 "sigs.k8s.io/cli-utils/pkg/print/table" 21 ) 22 23 type Printer struct { 24 IOStreams genericclioptions.IOStreams 25 } 26 27 func (t *Printer) Print(ch <-chan event.Event, _ common.DryRunStrategy, _ bool) error { 28 // Wait for the init event that will give us the set of 29 // resources. 30 var initEvent event.InitEvent 31 for e := range ch { 32 if e.Type == event.InitType { 33 initEvent = e.InitEvent 34 break 35 } 36 // If we get an error event, we just print it and 37 // exit. The error event signals a fatal error. 38 if e.Type == event.ErrorType { 39 return e.ErrorEvent.Err 40 } 41 } 42 // Create a new collector and initialize it with the resources 43 // we are interested in. 44 coll := newResourceStateCollector(initEvent.ActionGroups, t.IOStreams.Out) 45 46 stop := make(chan struct{}) 47 48 // Start the goroutine that is responsible for 49 // printing the latest state on a regular cadence. 50 printCompleted := t.runPrintLoop(coll, stop) 51 52 // Make the collector start listening on the eventChannel. 53 done := coll.Listen(ch) 54 55 // Block until all the collector has shut down. This means the 56 // eventChannel has been closed and all events have been processed. 57 var err error 58 for msg := range done { 59 err = msg.err 60 } 61 62 // Close the stop channel to notify the print goroutine that it should 63 // shut down. 64 close(stop) 65 66 // Wait until the printCompleted channel is closed. This means 67 // the printer has updated the UI with the latest state and 68 // exited from the goroutine. 69 <-printCompleted 70 71 if err != nil { 72 return err 73 } 74 // If no fatal errors happened, we will return a ResultError if 75 // one or more resources failed to apply/prune or reconcile. 76 return printcommon.ResultErrorFromStats(coll.stats) 77 } 78 79 // columns defines the columns we want to print 80 // TODO: We should have the number of columns and their widths be 81 // dependent on the space available. 82 var ( 83 unifiedStatusColumnDef = table.ColumnDef{ 84 // Column containing the overall progress. 85 ColumnName: "progress", 86 ColumnHeader: "PROGRESS", 87 ColumnWidth: 80, 88 PrintResourceFunc: printProgress, 89 } 90 91 alphaColumns = []table.ColumnDefinition{ 92 table.MustColumn("namespace"), 93 table.MustColumn("resource"), 94 95 // We are trying out a "single column" model here 96 unifiedStatusColumnDef, 97 } 98 ) 99 100 // runPrintLoop starts a new goroutine that will regularly fetch the 101 // latest state from the collector and update the table. 102 func (t *Printer) runPrintLoop(coll *resourceStateCollector, stop chan struct{}) chan struct{} { 103 finished := make(chan struct{}) 104 105 baseTablePrinter := table.BaseTablePrinter{ 106 IOStreams: t.IOStreams, 107 Columns: alphaColumns, 108 } 109 110 linesPrinted := baseTablePrinter.PrintTable(coll.LatestState(), 0) 111 112 go func() { 113 defer close(finished) 114 ticker := time.NewTicker(500 * time.Millisecond) 115 for { 116 select { 117 case <-stop: 118 ticker.Stop() 119 latestState := coll.LatestState() 120 linesPrinted = baseTablePrinter.PrintTable(latestState, linesPrinted) 121 _, _ = fmt.Fprint(t.IOStreams.Out, "\n") 122 return 123 case <-ticker.C: 124 latestState := coll.LatestState() 125 linesPrinted = baseTablePrinter.PrintTable(latestState, linesPrinted) 126 } 127 } 128 }() 129 return finished 130 } 131 132 func printProgress(w io.Writer, width int, r table.Resource) (int, error) { 133 var resInfo *resourceInfo 134 switch res := r.(type) { 135 case *resourceInfo: 136 resInfo = res 137 default: 138 return 0, fmt.Errorf("unexpected type %T", r) 139 } 140 141 text, details, err := getProgress(resInfo) 142 if err != nil { 143 return 0, err 144 } 145 if details != "" { 146 text += " " + details 147 } 148 149 if len(text) > width { 150 text = text[:width] 151 } 152 n, err := fmt.Fprint(w, text) 153 if err != nil { 154 return n, err 155 } 156 return len(text), err 157 } 158 159 func getProgress(resInfo *resourceInfo) (string, string, error) { 160 printStatus := false 161 var text string 162 var details string 163 switch resInfo.ResourceAction { 164 case event.ApplyAction: 165 switch resInfo.lastApplyEvent.Status { 166 case event.ApplyPending: 167 text = "PendingApply" 168 case event.ApplySuccessful: 169 text = "Applied" 170 printStatus = true 171 case event.ApplySkipped: 172 text = "SkippedApply" 173 174 case event.ApplyFailed: 175 text = "ApplyFailed" 176 177 default: 178 return "", "", fmt.Errorf("unknown ApplyStatus: %v", resInfo.lastApplyEvent.Status) 179 } 180 181 if resInfo.lastApplyEvent.Error != nil { 182 details = fmt.Sprintf("error:%+v", resInfo.lastApplyEvent.Error) 183 } 184 185 case event.PruneAction: 186 switch resInfo.lastPruneEvent.Status { 187 case event.PrunePending: 188 text = "PendingDeletion" 189 case event.PruneSuccessful: 190 text = "Deleted" 191 case event.PruneSkipped: 192 text = "DeletionSkipped" 193 case event.PruneFailed: 194 text = "DeletionFailed" 195 text += fmt.Sprintf(" %+v", resInfo.lastPruneEvent.Error) 196 197 default: 198 return "", "", fmt.Errorf("unknown PruneStatus: %v", resInfo.lastPruneEvent.Status) 199 } 200 201 if resInfo.lastPruneEvent.Error != nil { 202 details = fmt.Sprintf("error:%+v", resInfo.lastPruneEvent.Error) 203 } 204 205 default: 206 return "", "", fmt.Errorf("unknown ResourceAction %v", resInfo.ResourceAction) 207 } 208 209 rs := resInfo.ResourceStatus() 210 if printStatus && rs != nil { 211 s := rs.Status.String() 212 213 color, setColor := printcommon.ColorForStatus(rs.Status) 214 if setColor { 215 s = printcommon.SprintfWithColor(color, s) 216 } 217 218 text = s 219 220 if resInfo.ResourceAction == event.WaitAction { 221 text += " WaitStatus:" + resInfo.WaitStatus.String() 222 } 223 224 conditionStrings := getConditions(rs) 225 if rs.Status != status.CurrentStatus { 226 text += " Conditions:" + strings.Join(conditionStrings, ",") 227 } 228 229 var message string 230 if rs.Error != nil { 231 message = rs.Error.Error() 232 } else { 233 switch rs.Status { 234 case status.CurrentStatus: 235 // Don't print the message when things are OK 236 default: 237 message = rs.Message 238 } 239 } 240 241 if message != "" { 242 details += " message:" + message 243 } 244 245 // TODO: Need to wait for observedGeneration I think, as it is exiting before conditions are observed 246 } 247 248 return text, details, nil 249 } 250 251 func getConditions(rs *pollingevent.ResourceStatus) []string { 252 u := rs.Resource 253 if u == nil { 254 return nil 255 } 256 257 // TODO: Should we be using kstatus here? 258 259 conditions, found, err := unstructured.NestedSlice(u.Object, 260 "status", "conditions") 261 if !found || err != nil || len(conditions) == 0 { 262 return nil 263 } 264 265 var conditionStrings []string 266 for _, cond := range conditions { 267 condition := cond.(map[string]interface{}) 268 conditionType := condition["type"].(string) 269 conditionStatus := condition["status"].(string) 270 conditionReason := condition["reason"].(string) 271 lastTransitionTime := condition["lastTransitionTime"].(string) 272 273 // TODO: Colors should be line based, pending should be light gray 274 var color printcommon.Color 275 switch conditionStatus { 276 case "True": 277 color = printcommon.GREEN 278 case "False": 279 color = printcommon.RED 280 default: 281 color = printcommon.YELLOW 282 } 283 284 text := conditionReason 285 if text == "" { 286 text = conditionType 287 } 288 289 if lastTransitionTime != "" && color != printcommon.GREEN { 290 t, err := time.Parse(time.RFC3339, lastTransitionTime) 291 if err != nil { 292 klog.Warningf("failed to parse time %v: %v", lastTransitionTime, err) 293 } else { 294 text += " " + time.Since(t).Truncate(time.Second).String() 295 } 296 } 297 298 s := printcommon.SprintfWithColor(color, text) 299 conditionStrings = append(conditionStrings, s) 300 } 301 return conditionStrings 302 }