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  }