...

Source file src/edge-infra.dev/cmd/edge/monitoring/metermaid/cli/cli.go

Documentation: edge-infra.dev/cmd/edge/monitoring/metermaid/cli

     1  package cli
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"flag"
     7  	"fmt"
     8  	"io"
     9  	"log"
    10  	"os"
    11  	"strings"
    12  	"text/tabwriter"
    13  
    14  	gptext "github.com/jedib0t/go-pretty/v6/text"
    15  	"github.com/peterbourgon/ff/v3/ffcli"
    16  
    17  	"edge-infra.dev/pkg/lib/cli/commands"
    18  	metrics "edge-infra.dev/pkg/lib/gcp/monitoring/metrics"
    19  	"edge-infra.dev/pkg/lib/gcp/monitoring/monutil"
    20  )
    21  
    22  const (
    23  	mmTitle = `
    24  ___  ___     _           ___  ___      _     _ 
    25  |  \/  |    | |          |  \/  |     (_)   | |
    26  | .  . | ___| |_ ___ _ __| .  . | __ _ _  __| |
    27  | |\/| |/ _ \ __/ _ \ '__| |\/| |/ _` + "`" + ` | |/ _` + "`" + ` |
    28  | |  | |  __/ ||  __/ |  | |  | | (_| | | (_| |
    29  \_|  |_/\___|\__\___|_|  \_|  |_/\__,_|_|\__,_|`
    30  
    31  	mmBroomTitle = `
    32                          ('"-,_
    33                            "-,_"-,_
    34                                "` + "`" + `-,_"-,_
    35                                     "-,_"-,_
    36                                         ` + "`" + `-,_"-,_
    37                                             "-,_"-,_
    38  ___  ___     _           ___  ___      _     _ ` + "`" + `-,_"-,_
    39  |  \/  |    | |          |  \/  |     (_)   | |    "-,_"-,_    __,
    40  | .  . | ___| |_ ___ _ __| .  . | __ _ _  __| |        ` + "`" + `-,_"-,/_ /~=,_
    41  | |\/| |/ _ \ __/ _ \ '__| |\/| |/ _` + "`" + ` | |/ _` + "`" + ` |           "-/-,//~=,_~=,_
    42  | |  | |  __/ ||  __/ |  | |  | | (_| | | (_| |            \_//_~=,_~=,~
    43  \_|  |_/\___|\__\___|_|  \_|  |_/\__,_|_|\__,_|             '=,_~=,_~=
    44                                                                  ~=,_~`
    45  )
    46  
    47  var (
    48  	Stderr        io.Writer = os.Stderr
    49  	Stdout        io.Writer = os.Stdout
    50  	Stdin         io.Writer = os.Stdin
    51  	Println                 = fmt.Println
    52  	Fatalf                  = log.Fatalf
    53  	Errorf                  = fmt.Errorf
    54  	Sprintf                 = fmt.Sprintf
    55  	Printf                  = fmt.Printf
    56  	filterForeman           = false
    57  	multiProject            = false
    58  )
    59  
    60  func isBoolFlag(f *flag.Flag) bool {
    61  	bf, ok := f.Value.(interface {
    62  		IsBoolFlag() bool
    63  	})
    64  	return ok && bf.IsBoolFlag()
    65  }
    66  
    67  func countFlags(fs *flag.FlagSet) (n int) {
    68  	fs.VisitAll(func(*flag.Flag) { n++ })
    69  	return n
    70  }
    71  
    72  func newFlagSet(name string) *flag.FlagSet {
    73  	onError := flag.ExitOnError
    74  	fs := flag.NewFlagSet(name, onError)
    75  	fs.SetOutput(Stderr)
    76  	return fs
    77  }
    78  
    79  var (
    80  	projectID    string
    81  	metricPrefix string
    82  	metricName   string
    83  	jsonFormat   bool
    84  	monoFormat   bool
    85  	silent       bool
    86  )
    87  
    88  var globalFlags = func() *flag.FlagSet {
    89  	fset := flag.NewFlagSet("global", flag.ExitOnError)
    90  	fset.StringVar(&projectID, "project", "", "GCP project ID(s) for Metric Descriptor(s).")
    91  	fset.StringVar(&metricPrefix, "metric-prefix", "prometheus.googleapis.com", "GCP Metric Descriptor(s) URL prefix.")
    92  	fset.StringVar(&metricName, "metric-name", "", "Comma separated list of metric descriptor names. All metric descriptors will be acted upon if not provided.")
    93  	fset.BoolVar(&jsonFormat, "json", false, "Sets the output to JSON formatting instead of the default visual formatting.")
    94  	fset.BoolVar(&monoFormat, "monochrome", false, "Sets the visually formatted output to monochromatic (grayscale).")
    95  	fset.BoolVar(&silent, "silent", false, "Executes the specified action without confirmation.")
    96  	return fset
    97  }()
    98  
    99  // withGlobalFlags adds the global flags to the FlagSet.
   100  func withGlobalFlags(fset *flag.FlagSet) *flag.FlagSet {
   101  	globalFlags.VisitAll(func(f *flag.Flag) {
   102  		fset.Var(f.Value, f.Name, f.Usage)
   103  	})
   104  	return fset
   105  }
   106  
   107  // Run runs the CLI. The args do not include the binary name.
   108  func Run(args []string) (err error) {
   109  	rootfs := newFlagSet("metermaid")
   110  	rootCmd := &ffcli.Command{
   111  		Name:       "metermaid",
   112  		ShortUsage: "metermaid <subcommand> [command flags]",
   113  		ShortHelp:  "GCP Metric Descriptor alignment utility",
   114  		LongHelp: strings.TrimSpace(`
   115  For help on subcommands, add --help after: "metermaid get --help".
   116  `),
   117  		Subcommands: []*ffcli.Command{
   118  			alignCmd,
   119  			compareCmd,
   120  			deleteCmd,
   121  			getCmd,
   122  			listCmd,
   123  			commands.Version(),
   124  		},
   125  		FlagSet:   rootfs,
   126  		Exec:      func(context.Context, []string) error { return flag.ErrHelp },
   127  		UsageFunc: usageFunc,
   128  	}
   129  
   130  	for _, c := range rootCmd.Subcommands {
   131  		c.UsageFunc = usageFunc
   132  	}
   133  
   134  	if !silent && !jsonFormat {
   135  		monutil.ClearTerminal()
   136  		fmt.Printf("%s\n\n", monutil.White(mmTitle)) // title ASCII
   137  	}
   138  
   139  	if err = rootCmd.Parse(args); err != nil {
   140  		if errors.Is(err, flag.ErrHelp) {
   141  			return nil
   142  		}
   143  		return err
   144  	}
   145  
   146  	err = rootCmd.Run(context.Background())
   147  	if errors.Is(err, flag.ErrHelp) {
   148  		return nil
   149  	}
   150  	return err
   151  }
   152  
   153  func usageFunc(c *ffcli.Command) string {
   154  	var b strings.Builder
   155  	fmt.Fprintf(&b, "USAGE\n")
   156  	if c.ShortUsage != "" {
   157  		fmt.Fprintf(&b, "  %s\n", c.ShortUsage)
   158  	} else {
   159  		fmt.Fprintf(&b, "  %s\n", c.Name)
   160  	}
   161  	fmt.Fprintf(&b, "\n")
   162  
   163  	if c.LongHelp != "" {
   164  		fmt.Fprintf(&b, "%s\n\n", c.LongHelp)
   165  	}
   166  
   167  	if len(c.Subcommands) > 0 {
   168  		fmt.Fprintf(&b, "SUBCOMMANDS\n")
   169  		tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
   170  		for _, subcommand := range c.Subcommands {
   171  			fmt.Fprintf(tw, "  %s\t%s\n", subcommand.Name, subcommand.ShortHelp)
   172  		}
   173  		tw.Flush()
   174  		fmt.Fprintf(&b, "\n")
   175  	}
   176  
   177  	if countFlags(c.FlagSet) > 0 { //nolint
   178  		fmt.Fprintf(&b, "FLAGS\n")
   179  		tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
   180  		c.FlagSet.VisitAll(func(f *flag.Flag) {
   181  			var s string
   182  			name, usage := flag.UnquoteUsage(f)
   183  			if isBoolFlag(f) {
   184  				s = fmt.Sprintf("  --%s, --%s=false", f.Name, f.Name)
   185  			} else {
   186  				s = fmt.Sprintf("  --%s", f.Name) // Two spaces before --; see next two comments.
   187  				if len(name) > 0 {
   188  					s += " " + name
   189  				}
   190  			}
   191  			// Four spaces before the tab triggers good alignment
   192  			// for both 4- and 8-space tab stops.
   193  			s += "\n    \t"
   194  			s += strings.ReplaceAll(usage, "\n", "\n    \t")
   195  
   196  			if f.DefValue != "" {
   197  				s += fmt.Sprintf(" (default %s)", f.DefValue)
   198  			}
   199  
   200  			fmt.Fprintln(&b, s+"\n")
   201  		})
   202  		tw.Flush()
   203  		fmt.Fprintf(&b, "\n")
   204  	}
   205  	return strings.TrimSpace(b.String())
   206  }
   207  
   208  // checkListFlags validates the required List subcommand flags have been provided.
   209  func checkGlobalFlags() bool {
   210  	if len(projectID) == 0 {
   211  		fmt.Println("Error: no value specified for [project] - a valid project-id is required")
   212  		return false
   213  	}
   214  
   215  	// are multiple format options specified?
   216  	if jsonFormat && monoFormat {
   217  		fmt.Println("Error: json format option not supported with monochrome format selection - please choose only one of the formatting options")
   218  		return false
   219  	}
   220  
   221  	// multiple project IDs specified?
   222  	if monutil.IsList(projectID) {
   223  		multiProject = true
   224  	}
   225  
   226  	// is the projectID foreman and part of a list?
   227  	if !monutil.IsForeman(projectID) && multiProject {
   228  		filterForeman = true
   229  	}
   230  
   231  	// query multiple metric names
   232  	if monutil.IsList(metricName) {
   233  		mNames := strings.Split(strings.ReplaceAll(metricName, " ", ""), ",")
   234  		for i := 0; i < len(mNames); i++ {
   235  			if metrics.HasMetricPrefix(mNames[i]) {
   236  				fmt.Println("Error: metric prefix should not be included as part of the specified metric name - use the '--metric-prefix' to specify the metric URL")
   237  				return false
   238  			}
   239  		}
   240  
   241  		// only single metric name searches are supported currently
   242  		if len(mNames) < 1 {
   243  			fmt.Println("Error: only single metric name searches are currently supported")
   244  			return false
   245  		}
   246  	}
   247  
   248  	// disable terminal colors?
   249  	if monoFormat {
   250  		gptext.DisableColors()
   251  	}
   252  
   253  	return true
   254  }
   255  
   256  // retrieves multiple project's descriptors
   257  func retrieveDescriptors(ctx context.Context, p []string) (*metrics.Descriptors, error) {
   258  	var descs metrics.Descriptors
   259  
   260  	for i := 0; i < len(p); i++ {
   261  		// create the client
   262  		client, err := metrics.New(ctx, p[i])
   263  		if err != nil {
   264  			return nil, Errorf("failed to create metric client: %w", err)
   265  		}
   266  
   267  		// retrieve the metric descriptors list for the project
   268  		d, err := client.ListDescriptors(metricPrefix, metricName)
   269  		if err != nil {
   270  			return nil, Errorf("failed to retrieve metric descriptors: %w", err)
   271  		}
   272  
   273  		// combine the metric descriptors lists
   274  		descs, err = metrics.AppendDescriptors(descs.Descriptor, d.Descriptor)
   275  		if err != nil {
   276  			return nil, Errorf("failed to append metric descriptors lists: %w", err)
   277  		}
   278  	}
   279  	return &descs, nil
   280  }
   281  

View as plain text