package cli import ( "context" "errors" "flag" "fmt" "io" "log" "os" "path/filepath" "strings" "text/tabwriter" "time" "github.com/peterbourgon/ff/v3/ffcli" "edge-infra.dev/pkg/lib/build/bazel" "edge-infra.dev/pkg/lib/cli/commands" dashmgr "edge-infra.dev/pkg/lib/gcp/monitoring/dashboardmanager" "edge-infra.dev/pkg/lib/gcp/monitoring/monutil" ) var ( Stderr io.Writer = os.Stderr Stdout io.Writer = os.Stdout Println = log.Println Printf = log.Printf Fatalf = log.Fatalf Errorf = fmt.Errorf Sprintf = fmt.Sprintf ) func isBoolFlag(f *flag.Flag) bool { bf, ok := f.Value.(interface { IsBoolFlag() bool }) return ok && bf.IsBoolFlag() } func countFlags(fs *flag.FlagSet) (n int) { fs.VisitAll(func(*flag.Flag) { n++ }) return n } func newFlagSet(name string) *flag.FlagSet { onError := flag.ExitOnError fs := flag.NewFlagSet(name, onError) fs.SetOutput(Stderr) return fs } var ( projectID string verbose = false wd string tPath string ) var globalFlags = func() *flag.FlagSet { fset := flag.NewFlagSet("global", flag.ExitOnError) fset.StringVar(&projectID, "project", "", "GCP project ID for dashboard management.") fset.BoolVar(&verbose, "verbose", false, "Enable verbose output of command") return fset }() // withGlobalFlags adds the global flags to the FlagSet. func withGlobalFlags(fset *flag.FlagSet) *flag.FlagSet { globalFlags.VisitAll(func(f *flag.Flag) { fset.Var(f.Value, f.Name, f.Usage) }) return fset } // Run runs the CLI. The args do not include the binary name. func Run(args []string) (err error) { rootfs := newFlagSet("dashman") rootCmd := &ffcli.Command{ Name: "dashman", // ShortUsage: "dashman [flags] [command flags]", ShortUsage: "dashman [command flags]", ShortHelp: "GCP Dashboard Manager Utility", LongHelp: strings.TrimSpace(` For help on subcommands, add --help after: "dashman sync --help". This CLI is still under active development. Commands and flags may change in the future. `), Subcommands: []*ffcli.Command{ addCmd, deleteCmd, getCmd, syncCmd, updateCmd, commands.Version(), }, FlagSet: rootfs, Exec: func(context.Context, []string) error { return flag.ErrHelp }, UsageFunc: usageFunc, } wd, err = bazel.ResolveWd() if err != nil { return errors.New(Sprintf("failed to determine current working directory: %s", err.Error())) } err = os.Chdir(wd) if err != nil { return err } for _, c := range rootCmd.Subcommands { c.UsageFunc = usageFunc } if err = rootCmd.Parse(args); err != nil { if errors.Is(err, flag.ErrHelp) { return nil } return err } dashmgr.Verbose = verbose err = rootCmd.Run(context.Background()) if errors.Is(err, flag.ErrHelp) { return nil } return err } func usageFunc(c *ffcli.Command) string { var b strings.Builder fmt.Fprintf(&b, "USAGE\n") if c.ShortUsage != "" { fmt.Fprintf(&b, " %s\n", c.ShortUsage) } else { fmt.Fprintf(&b, " %s\n", c.Name) } fmt.Fprintf(&b, "\n") if c.LongHelp != "" { fmt.Fprintf(&b, "%s\n\n", c.LongHelp) } if len(c.Subcommands) > 0 { fmt.Fprintf(&b, "SUBCOMMANDS\n") tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0) for _, subcommand := range c.Subcommands { fmt.Fprintf(tw, " %s\t%s\n", subcommand.Name, subcommand.ShortHelp) } tw.Flush() fmt.Fprintf(&b, "\n") } if countFlags(c.FlagSet) > 0 { //nolint fmt.Fprintf(&b, "FLAGS\n") tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0) c.FlagSet.VisitAll(func(f *flag.Flag) { var s string name, usage := flag.UnquoteUsage(f) if isBoolFlag(f) { s = fmt.Sprintf(" --%s, --%s=false", f.Name, f.Name) } else { s = fmt.Sprintf(" --%s", f.Name) // Two spaces before --; see next two comments. if len(name) > 0 { s += " " + name } } // Four spaces before the tab triggers good alignment // for both 4- and 8-space tab stops. s += "\n \t" s += strings.ReplaceAll(usage, "\n", "\n \t") if f.DefValue != "" { s += fmt.Sprintf(" (default %s)", f.DefValue) } fmt.Fprintln(&b, s+"\n") }) tw.Flush() fmt.Fprintf(&b, "\n") } return strings.TrimSpace(b.String()) } // checks the provided path for validity as file or folder source func checkPath(path string, folder bool) bool { var err error tPath, err = filepath.Abs(path) if err != nil { Printf("Error evaluating absolute path: %s\n", err.Error()) return false } if !monutil.FileExists(path) { Println("Path specified is invalid") return false } if !monutil.IsDirectory(path) && folder { Println("Path specified appears to be a file but folder path is required") return false } return true } // returns the date plus the number of days specified func getDate(numDays int) string { currTime := time.Now() newTime := currTime.AddDate(0, 0, numDays) return newTime.Format("2006-01-02") } // enables verbose loging func vPrintln(msg string) { if verbose { fmt.Println(msg) } } // enables verbose formatted loging func vPrintf(str string, msg ...interface{}) { if verbose { fmt.Printf(str, msg...) } } // splits a string array and removes whitespace func strArray(str string) []string { array := strings.Split(str, ",") for i := 0; i < len(array); i++ { array[i] = strings.TrimSpace(array[i]) } return array }