...

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

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

     1  package cli
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"flag"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"path/filepath"
    11  	"regexp"
    12  	"strings"
    13  	"text/tabwriter"
    14  
    15  	"log"
    16  
    17  	"edge-infra.dev/pkg/lib/logging"
    18  
    19  	"github.com/peterbourgon/ff/v3/ffcli"
    20  
    21  	"edge-infra.dev/pkg/lib/build/bazel"
    22  	"edge-infra.dev/pkg/lib/cli/commands"
    23  )
    24  
    25  var (
    26  	Stderr  io.Writer = os.Stderr
    27  	Println           = log.Println
    28  	Fatalf            = log.Fatalf
    29  	Errorf            = fmt.Errorf
    30  	Sprintf           = fmt.Sprintf
    31  )
    32  
    33  var logger = logging.NewLogger().WithName("alertman-log")
    34  
    35  func isBoolFlag(f *flag.Flag) bool {
    36  	bf, ok := f.Value.(interface {
    37  		IsBoolFlag() bool
    38  	})
    39  	return ok && bf.IsBoolFlag()
    40  }
    41  
    42  func countFlags(fs *flag.FlagSet) (n int) {
    43  	fs.VisitAll(func(*flag.Flag) { n++ })
    44  	return n
    45  }
    46  
    47  func newFlagSet(name string) *flag.FlagSet {
    48  	onError := flag.ExitOnError
    49  	fs := flag.NewFlagSet(name, onError)
    50  	fs.SetOutput(Stderr)
    51  	return fs
    52  }
    53  
    54  var (
    55  	projectID string
    56  	wd        string
    57  	tPath     string
    58  )
    59  
    60  var globalFlags = func() *flag.FlagSet {
    61  	fset := flag.NewFlagSet("global", flag.ExitOnError)
    62  	fset.StringVar(&projectID, "project", "", "GCP Project ID where alert policy is located.")
    63  	return fset
    64  }()
    65  
    66  // withGlobalFlags adds the global flags to the FlagSet.
    67  func withGlobalFlags(fset *flag.FlagSet) *flag.FlagSet {
    68  	globalFlags.VisitAll(func(f *flag.Flag) {
    69  		fset.Var(f.Value, f.Name, f.Usage)
    70  	})
    71  	return fset
    72  }
    73  
    74  // Run runs the CLI. The args do not include the binary name.
    75  func Run(args []string) (err error) {
    76  	rootfs := newFlagSet("alertman")
    77  	rootCmd := &ffcli.Command{
    78  		Name:       "alertman",
    79  		ShortUsage: "alertman <subcommand> [command flags]",
    80  		ShortHelp:  "GCP alert manager utility",
    81  		LongHelp: strings.TrimSpace(`
    82  For help on subcommands, add --help after: "alertman get --help".
    83  `),
    84  		Subcommands: []*ffcli.Command{
    85  			getCmd,
    86  			deleteCmd,
    87  			createCmd,
    88  			updateCmd,
    89  			syncCmd,
    90  			commands.Version(),
    91  		},
    92  		FlagSet:   rootfs,
    93  		Exec:      func(context.Context, []string) error { return flag.ErrHelp },
    94  		UsageFunc: usageFunc,
    95  	}
    96  
    97  	wd, err = bazel.ResolveWd()
    98  	if err != nil {
    99  		return errors.New(Sprintf("failed to determine current working directory: %s", err.Error()))
   100  	}
   101  
   102  	err = os.Chdir(wd)
   103  	if err != nil {
   104  		return err
   105  	}
   106  
   107  	for _, c := range rootCmd.Subcommands {
   108  		c.UsageFunc = usageFunc
   109  	}
   110  
   111  	if err = rootCmd.Parse(args); err != nil {
   112  		if errors.Is(err, flag.ErrHelp) {
   113  			return nil
   114  		}
   115  		return err
   116  	}
   117  
   118  	err = rootCmd.Run(context.Background())
   119  	if errors.Is(err, flag.ErrHelp) {
   120  		return nil
   121  	}
   122  	return err
   123  }
   124  
   125  func usageFunc(c *ffcli.Command) string {
   126  	var b strings.Builder
   127  
   128  	fmt.Fprintf(&b, "USAGE\n")
   129  	if c.ShortUsage != "" {
   130  		fmt.Fprintf(&b, "  %s\n", c.ShortUsage)
   131  	} else {
   132  		fmt.Fprintf(&b, "  %s\n", c.Name)
   133  	}
   134  	fmt.Fprintf(&b, "\n")
   135  
   136  	if c.LongHelp != "" {
   137  		fmt.Fprintf(&b, "%s\n\n", c.LongHelp)
   138  	}
   139  
   140  	switch {
   141  	case len(c.Subcommands) > 0:
   142  		fmt.Fprintf(&b, "SUBCOMMANDS\n")
   143  		tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
   144  		for _, subcommand := range c.Subcommands {
   145  			fmt.Fprintf(tw, "  %s\t%s\n", subcommand.Name, subcommand.ShortHelp)
   146  		}
   147  		tw.Flush()
   148  		fmt.Fprintf(&b, "\n")
   149  	case countFlags(c.FlagSet) > 0:
   150  		fmt.Fprintf(&b, "FLAGS\n")
   151  		tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
   152  		c.FlagSet.VisitAll(func(f *flag.Flag) {
   153  			var s string
   154  			name, usage := flag.UnquoteUsage(f)
   155  			if isBoolFlag(f) {
   156  				s = fmt.Sprintf("  --%s, --%s=false", f.Name, f.Name)
   157  			} else {
   158  				s = fmt.Sprintf("  --%s", f.Name) // Two spaces before --; see next two comments.
   159  				if len(name) > 0 {
   160  					s += " " + name
   161  				}
   162  			}
   163  			// Four spaces before the tab triggers good alignment
   164  			// for both 4- and 8-space tab stops.
   165  			s += "\n    \t"
   166  			s += strings.ReplaceAll(usage, "\n", "\n    \t")
   167  
   168  			if f.DefValue != "" {
   169  				s += fmt.Sprintf(" (default %s)", f.DefValue)
   170  			}
   171  
   172  			fmt.Fprintln(&b, s+"\n")
   173  		})
   174  		tw.Flush()
   175  		fmt.Fprintf(&b, "\n")
   176  	}
   177  	return strings.TrimSpace(b.String())
   178  }
   179  
   180  // checks the provided path for validity as file or folder source.
   181  func checkPath(path string, folder bool) bool {
   182  	var err error
   183  
   184  	tPath, err = filepath.Abs(path)
   185  	if err != nil {
   186  		fmt.Printf("Error evaluating absolute path: %s\n", err.Error())
   187  		return false
   188  	}
   189  
   190  	fileInfo, err := os.Stat(tPath)
   191  	if os.IsNotExist(err) {
   192  		fmt.Println("Path specified is invalid")
   193  		return false
   194  	}
   195  
   196  	if !fileInfo.IsDir() && folder {
   197  		fmt.Println("Path specified appears to be a file but folder path is required")
   198  		return false
   199  	}
   200  	return true
   201  }
   202  
   203  // cleanup and splits a string to a string array based on regexp.
   204  func strArray(str string, flag bool) ([]string, error) {
   205  	content := str
   206  	// Regex pattern captures "key: value" pair from the content.
   207  	pattern := regexp.MustCompile(`(?:((?P<key>[A-Za-z0-9_-]+)(|\s+):(|\s+)(?P<value>[A-Za-z0-9_-]+))|((?P<key>[A-Za-z0-9_-]+)(|\s+):(|\s+)))`)
   208  
   209  	// Template to convert "key: value" to "key=value" by referencing the values captured by the regex pattern.
   210  	template := "$key\n$value\n"
   211  	templateDisplay := "$key:$value, "
   212  	result := []byte{}
   213  	resultDisplay := []byte{}
   214  
   215  	// For each match of the regex in the content.
   216  	for _, submatches := range pattern.FindAllStringSubmatchIndex(content, -1) {
   217  		// Apply the captured submatches to the template and append the output to the result.
   218  		result = pattern.ExpandString(result, template, content, submatches)
   219  		resultDisplay = pattern.ExpandString(resultDisplay, templateDisplay, content, submatches)
   220  	}
   221  
   222  	if flag {
   223  		str1 := strings.TrimSpace(string(result)) // removes any trailling spaces.
   224  		zp := regexp.MustCompile(`\n`)            // regexp for space.
   225  		array := zp.Split(str1, -1)
   226  		for i := 0; i < len(array); i++ {
   227  			array[i] = strings.TrimSpace(array[i])
   228  		}
   229  		// validate the key/value array length is a multiple of 2 else prompt for error.
   230  		res := len(array) % 2
   231  		if res != 0 {
   232  			err := fmt.Errorf("the UserLabels {%s} is missing a Key or Value", string(resultDisplay))
   233  			return nil, err
   234  		}
   235  		return array, nil
   236  	}
   237  	str1 := string(result)         // removes any trailling spaces.
   238  	zp := regexp.MustCompile(`\n`) // regexp for space.
   239  	array := zp.Split(str1, -1)
   240  	for i := 0; i < len(array); i++ {
   241  		array[i] = strings.TrimSpace(array[i])
   242  	}
   243  	return array, nil
   244  }
   245  

View as plain text