...

Source file src/sigs.k8s.io/cli-utils/cmd/status/cmdstatus.go

Documentation: sigs.k8s.io/cli-utils/cmd/status

     1  // Copyright 2020 The Kubernetes Authors.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package status
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/spf13/cobra"
    13  	"k8s.io/cli-runtime/pkg/genericclioptions"
    14  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    15  	"k8s.io/kubectl/pkg/util/slice"
    16  	"sigs.k8s.io/cli-utils/cmd/flagutils"
    17  	"sigs.k8s.io/cli-utils/cmd/status/printers"
    18  	"sigs.k8s.io/cli-utils/cmd/status/printers/printer"
    19  	"sigs.k8s.io/cli-utils/pkg/apply/poller"
    20  	"sigs.k8s.io/cli-utils/pkg/common"
    21  	"sigs.k8s.io/cli-utils/pkg/inventory"
    22  	"sigs.k8s.io/cli-utils/pkg/kstatus/polling"
    23  	"sigs.k8s.io/cli-utils/pkg/kstatus/polling/aggregator"
    24  	"sigs.k8s.io/cli-utils/pkg/kstatus/polling/collector"
    25  	"sigs.k8s.io/cli-utils/pkg/kstatus/polling/event"
    26  	"sigs.k8s.io/cli-utils/pkg/kstatus/status"
    27  	"sigs.k8s.io/cli-utils/pkg/manifestreader"
    28  	"sigs.k8s.io/cli-utils/pkg/object"
    29  	printcommon "sigs.k8s.io/cli-utils/pkg/print/common"
    30  	pkgprinters "sigs.k8s.io/cli-utils/pkg/printers"
    31  )
    32  
    33  const (
    34  	Known   = "known"
    35  	Current = "current"
    36  	Deleted = "deleted"
    37  	Forever = "forever"
    38  )
    39  
    40  const (
    41  	Local  = "local"
    42  	Remote = "remote"
    43  )
    44  
    45  var (
    46  	PollUntilOptions = []string{Known, Current, Deleted, Forever}
    47  )
    48  
    49  func GetRunner(ctx context.Context, factory cmdutil.Factory,
    50  	invFactory inventory.ClientFactory, loader Loader) *Runner {
    51  	r := &Runner{
    52  		ctx:               ctx,
    53  		factory:           factory,
    54  		invFactory:        invFactory,
    55  		loader:            loader,
    56  		PollerFactoryFunc: pollerFactoryFunc,
    57  	}
    58  	c := &cobra.Command{
    59  		Use:     "status (DIRECTORY | STDIN)",
    60  		PreRunE: r.preRunE,
    61  		RunE:    r.runE,
    62  	}
    63  	c.Flags().DurationVar(&r.period, "poll-period", 2*time.Second,
    64  		"Polling period for resource statuses.")
    65  	c.Flags().StringVar(&r.pollUntil, "poll-until", "known",
    66  		"When to stop polling. Must be one of 'known', 'current', 'deleted', or 'forever'.")
    67  	c.Flags().StringVar(&r.output, "output", "events", "Output format.")
    68  	c.Flags().DurationVar(&r.timeout, "timeout", 0,
    69  		"How long to wait before exiting")
    70  	c.Flags().StringVar(&r.invType, "inv-type", Local, "Type of the inventory info, must be local or remote")
    71  	c.Flags().StringVar(&r.inventoryNames, "inv-names", "", "Names of targeted inventory: inv1,inv2,...")
    72  	c.Flags().StringVar(&r.namespaces, "namespaces", "", "Names of targeted namespaces: ns1,ns2,...")
    73  	c.Flags().StringVar(&r.statuses, "statuses", "", "Targeted status: st1,st2...")
    74  
    75  	r.Command = c
    76  	return r
    77  }
    78  
    79  func Command(ctx context.Context, f cmdutil.Factory,
    80  	invFactory inventory.ClientFactory, loader Loader) *cobra.Command {
    81  	return GetRunner(ctx, f, invFactory, loader).Command
    82  }
    83  
    84  // Runner captures the parameters for the command and contains
    85  // the run function.
    86  type Runner struct {
    87  	ctx        context.Context
    88  	Command    *cobra.Command
    89  	factory    cmdutil.Factory
    90  	invFactory inventory.ClientFactory
    91  	loader     Loader
    92  
    93  	period    time.Duration
    94  	pollUntil string
    95  	timeout   time.Duration
    96  	output    string
    97  
    98  	invType          string
    99  	inventoryNames   string
   100  	inventoryNameSet map[string]bool
   101  	namespaces       string
   102  	namespaceSet     map[string]bool
   103  	statuses         string
   104  	statusSet        map[string]bool
   105  
   106  	PollerFactoryFunc func(cmdutil.Factory) (poller.Poller, error)
   107  }
   108  
   109  func (r *Runner) preRunE(*cobra.Command, []string) error {
   110  	if !slice.ContainsString(PollUntilOptions, r.pollUntil, nil) {
   111  		return fmt.Errorf("pollUntil must be one of %s", strings.Join(PollUntilOptions, ","))
   112  	}
   113  
   114  	if found := pkgprinters.ValidatePrinterType(r.output); !found {
   115  		return fmt.Errorf("unknown output type %q", r.output)
   116  	}
   117  
   118  	if r.invType != Local && r.invType != Remote {
   119  		return fmt.Errorf("inv-type flag should be either local or remote")
   120  	}
   121  
   122  	if r.invType == Local && r.inventoryNames != "" {
   123  		return fmt.Errorf("inv-names flag should only be used when inv-type is set to remote")
   124  	}
   125  
   126  	if r.inventoryNames != "" {
   127  		r.inventoryNameSet = make(map[string]bool)
   128  		for _, name := range strings.Split(r.inventoryNames, ",") {
   129  			r.inventoryNameSet[name] = true
   130  		}
   131  	}
   132  
   133  	if r.namespaces != "" {
   134  		r.namespaceSet = make(map[string]bool)
   135  		for _, ns := range strings.Split(r.namespaces, ",") {
   136  			r.namespaceSet[ns] = true
   137  		}
   138  	}
   139  
   140  	if r.statuses != "" {
   141  		r.statusSet = make(map[string]bool)
   142  		for _, st := range strings.Split(r.statuses, ",") {
   143  			parsedST := strings.ToLower(st)
   144  			r.statusSet[parsedST] = true
   145  		}
   146  	}
   147  
   148  	return nil
   149  }
   150  
   151  // Load inventory info from local storage
   152  // and get info from the cluster based on the local info
   153  // wrap it to be a map mapping from string to objectMetadataSet
   154  func (r *Runner) loadInvFromDisk(cmd *cobra.Command, args []string) (*printer.PrintData, error) {
   155  	inv, err := r.loader.GetInvInfo(cmd, args)
   156  	if err != nil {
   157  		return nil, err
   158  	}
   159  
   160  	invClient, err := r.invFactory.NewClient(r.factory)
   161  	if err != nil {
   162  		return nil, err
   163  	}
   164  
   165  	// Based on the inventory template manifest we look up the inventory
   166  	// from the live state using the inventory client.
   167  	identifiers, err := invClient.GetClusterObjs(inv)
   168  	if err != nil {
   169  		return nil, err
   170  	}
   171  
   172  	printData := printer.PrintData{
   173  		Identifiers: object.ObjMetadataSet{},
   174  		InvNameMap:  make(map[object.ObjMetadata]string),
   175  		StatusSet:   r.statusSet,
   176  	}
   177  
   178  	for _, obj := range identifiers {
   179  		// check if the object is under one of the targeted namespaces
   180  		if _, ok := r.namespaceSet[obj.Namespace]; ok || len(r.namespaceSet) == 0 {
   181  			// add to the map for future reference
   182  			printData.InvNameMap[obj] = inv.Name()
   183  			// append to identifiers
   184  			printData.Identifiers = append(printData.Identifiers, obj)
   185  		}
   186  	}
   187  	return &printData, nil
   188  }
   189  
   190  // Retrieve a list of inventory object from the cluster
   191  func (r *Runner) listInvFromCluster() (*printer.PrintData, error) {
   192  	invClient, err := r.invFactory.NewClient(r.factory)
   193  	if err != nil {
   194  		return nil, err
   195  	}
   196  
   197  	// initialize maps in printData
   198  	printData := printer.PrintData{
   199  		Identifiers: object.ObjMetadataSet{},
   200  		InvNameMap:  make(map[object.ObjMetadata]string),
   201  		StatusSet:   r.statusSet,
   202  	}
   203  
   204  	identifiersMap, err := invClient.ListClusterInventoryObjs(r.ctx)
   205  	if err != nil {
   206  		return nil, err
   207  	}
   208  
   209  	for invName, identifiers := range identifiersMap {
   210  		// Check if there are targeted inventory names and include the current inventory name
   211  		if _, ok := r.inventoryNameSet[invName]; !ok && len(r.inventoryNameSet) != 0 {
   212  			continue
   213  		}
   214  		// Filter objects
   215  		for _, obj := range identifiers {
   216  			// check if the object is under one of the targeted namespaces
   217  			if _, ok := r.namespaceSet[obj.Namespace]; ok || len(r.namespaceSet) == 0 {
   218  				// add to the map for future reference
   219  				printData.InvNameMap[obj] = invName
   220  				// append to identifiers
   221  				printData.Identifiers = append(printData.Identifiers, obj)
   222  			}
   223  		}
   224  	}
   225  	return &printData, nil
   226  }
   227  
   228  // runE implements the logic of the command and will delegate to the
   229  // poller to compute status for each of the resources. One of the printer
   230  // implementations takes care of printing the output.
   231  func (r *Runner) runE(cmd *cobra.Command, args []string) error {
   232  	var printData *printer.PrintData
   233  	var err error
   234  	switch r.invType {
   235  	case Local:
   236  		if len(args) != 0 {
   237  			printcommon.SprintfWithColor(printcommon.YELLOW,
   238  				"Warning: Path is assigned while list flag is enabled, ignore the path")
   239  		}
   240  		printData, err = r.loadInvFromDisk(cmd, args)
   241  	case Remote:
   242  		printData, err = r.listInvFromCluster()
   243  	default:
   244  		return fmt.Errorf("invType must be either local or remote")
   245  	}
   246  	if err != nil {
   247  		return err
   248  	}
   249  
   250  	// Exit here if the inventory is empty.
   251  	if len(printData.Identifiers) == 0 {
   252  		_, _ = fmt.Fprint(cmd.OutOrStdout(), "no resources found in the inventory\n")
   253  		return nil
   254  	}
   255  
   256  	statusPoller, err := r.PollerFactoryFunc(r.factory)
   257  	if err != nil {
   258  		return err
   259  	}
   260  
   261  	// Fetch a printer implementation based on the desired output format as
   262  	// specified in the output flag.
   263  	printer, err := printers.CreatePrinter(r.output, genericclioptions.IOStreams{
   264  		In:     cmd.InOrStdin(),
   265  		Out:    cmd.OutOrStdout(),
   266  		ErrOut: cmd.ErrOrStderr(),
   267  	}, printData)
   268  	if err != nil {
   269  		return fmt.Errorf("error creating printer: %w", err)
   270  	}
   271  
   272  	// If the user has specified a timeout, we create a context with timeout,
   273  	// otherwise we create a context with cancel.
   274  	ctx := cmd.Context()
   275  	var cancel func()
   276  	if r.timeout != 0 {
   277  		ctx, cancel = context.WithTimeout(ctx, r.timeout)
   278  	} else {
   279  		ctx, cancel = context.WithCancel(ctx)
   280  	}
   281  	defer cancel()
   282  
   283  	// Choose the appropriate ObserverFunc based on the criteria for when
   284  	// the command should exit.
   285  	var cancelFunc collector.ObserverFunc
   286  	switch r.pollUntil {
   287  	case "known":
   288  		cancelFunc = allKnownNotifierFunc(cancel)
   289  	case "current":
   290  		cancelFunc = desiredStatusNotifierFunc(cancel, status.CurrentStatus)
   291  	case "deleted":
   292  		cancelFunc = desiredStatusNotifierFunc(cancel, status.NotFoundStatus)
   293  	case "forever":
   294  		cancelFunc = func(*collector.ResourceStatusCollector, event.Event) {}
   295  	default:
   296  		return fmt.Errorf("unknown value for pollUntil: %q", r.pollUntil)
   297  	}
   298  
   299  	eventChannel := statusPoller.Poll(ctx, printData.Identifiers, polling.PollOptions{
   300  		PollInterval: r.period,
   301  	})
   302  
   303  	return printer.Print(eventChannel, printData.Identifiers, cancelFunc)
   304  }
   305  
   306  // desiredStatusNotifierFunc returns an Observer function for the
   307  // ResourceStatusCollector that will cancel the context (using the cancelFunc)
   308  // when all resources have reached the desired status.
   309  func desiredStatusNotifierFunc(cancelFunc context.CancelFunc,
   310  	desired status.Status) collector.ObserverFunc {
   311  	return func(rsc *collector.ResourceStatusCollector, _ event.Event) {
   312  		var rss []*event.ResourceStatus
   313  		for _, rs := range rsc.ResourceStatuses {
   314  			rss = append(rss, rs)
   315  		}
   316  		aggStatus := aggregator.AggregateStatus(rss, desired)
   317  		if aggStatus == desired {
   318  			cancelFunc()
   319  		}
   320  	}
   321  }
   322  
   323  // allKnownNotifierFunc returns an Observer function for the
   324  // ResourceStatusCollector that will cancel the context (using the cancelFunc)
   325  // when all resources have a known status.
   326  func allKnownNotifierFunc(cancelFunc context.CancelFunc) collector.ObserverFunc {
   327  	return func(rsc *collector.ResourceStatusCollector, _ event.Event) {
   328  		for _, rs := range rsc.ResourceStatuses {
   329  			if rs.Status == status.UnknownStatus {
   330  				return
   331  			}
   332  		}
   333  		cancelFunc()
   334  	}
   335  }
   336  
   337  func pollerFactoryFunc(f cmdutil.Factory) (poller.Poller, error) {
   338  	return polling.NewStatusPollerFromFactory(f, polling.Options{})
   339  }
   340  
   341  type Loader interface {
   342  	GetInvInfo(cmd *cobra.Command, args []string) (inventory.Info, error)
   343  }
   344  
   345  type InventoryLoader struct {
   346  	Loader manifestreader.ManifestLoader
   347  }
   348  
   349  func NewInventoryLoader(loader manifestreader.ManifestLoader) *InventoryLoader {
   350  	return &InventoryLoader{
   351  		Loader: loader,
   352  	}
   353  }
   354  
   355  func (ir *InventoryLoader) GetInvInfo(cmd *cobra.Command, args []string) (inventory.Info, error) {
   356  	_, err := common.DemandOneDirectory(args)
   357  	if err != nil {
   358  		return nil, err
   359  	}
   360  
   361  	reader, err := ir.Loader.ManifestReader(cmd.InOrStdin(), flagutils.PathFromArgs(args))
   362  	if err != nil {
   363  		return nil, err
   364  	}
   365  	objs, err := reader.Read()
   366  	if err != nil {
   367  		return nil, err
   368  	}
   369  
   370  	invObj, _, err := inventory.SplitUnstructureds(objs)
   371  	if err != nil {
   372  		return nil, err
   373  	}
   374  	inv := inventory.WrapInventoryInfoObj(invObj)
   375  	return inv, nil
   376  }
   377  

View as plain text