// Package cmd contains the billman CLI package cmd import ( "context" "flag" "fmt" "os" "strconv" "strings" "github.com/peterbourgon/ff/v3/ffcli" "edge-infra.dev/pkg/edge/monitoring/billman/costs" "edge-infra.dev/pkg/edge/monitoring/billman/edgesql" "edge-infra.dev/pkg/lib/cli/commands" ) const ( // TODO: see if the Google Cloud pricing api can be used instead. // https://cloud.google.com/billing/v1/how-tos/catalog-api loggingRate = .50 // $0.50/GiB for logs ingested metricsRate = .060 //$0.060/million samples: first 0-50 billion samples name = "billman" ) // Config for the root command, including flags and types that should be // available to each subcommand. type Config struct { Cluster edgesql.EdgeCluster ClusterID string Period string TopLevelProjectID string Options costs.Options LoggingRate float64 MetricsRate float64 DisplayType string } var ( clusterID string period string topLevelProjectID string output string dbHost string dbUser string dbName string dbPassword string showHeader bool showDisclaimer bool ) func SQLCreds(key string, defaultValue string) string { value := os.Getenv(key) if value == "" { return defaultValue } return value } var GlobalFlags = func() *flag.FlagSet { fset := flag.NewFlagSet("global", flag.ExitOnError) fset.StringVar(&clusterID, "clusterID", "", "(required) Edge cluster ID") fset.StringVar(&period, "period", "1d", "time window for the query. e.g.: 1d,7d") fset.StringVar(&topLevelProjectID, "topLevelProjectID", "", "(required) project id for the foreman cluster") fset.StringVar(&output, "output", "tab", "csv or tab output") fset.StringVar(&dbHost, "dbHost", SQLCreds("SQL_CONNECTION_NAME", "unknown"), "(optional) SQL DB connection name") fset.StringVar(&dbUser, "dbUser", SQLCreds("SQL_USER", "unknown"), "(optional) SQL DB user") fset.StringVar(&dbName, "dbName", SQLCreds("SQL_DB_NAME", "unknown"), "(optional) SQL DB name") fset.StringVar(&dbPassword, "dbPassword", SQLCreds("SQL_PASSWORD", "unknown"), "(optional) SQL DB password") fset.BoolVar(&showHeader, "show-header", true, "if false don't print the table header") fset.BoolVar(&showDisclaimer, "show-disclaimer", true, "if false don't print the disclaimer") return fset }() func WithGlobalFlags(fset *flag.FlagSet) *flag.FlagSet { GlobalFlags.VisitAll(func(f *flag.Flag) { fset.Var(f.Value, f.Name, f.Usage) }) return fset } func CheckRequiredFlags(flags *flag.FlagSet) error { missingFlag := false flags.VisitAll(func(f *flag.Flag) { if strings.TrimSpace(f.Value.String()) == "" { missingFlag = true } }) if missingFlag { return flag.ErrHelp } return nil } func New() (*ffcli.Command, *Config) { cfg := Config{} fs := flag.NewFlagSet(name, flag.ExitOnError) return &ffcli.Command{ Name: name, ShortUsage: name + " [subcommand] [flags]", FlagSet: WithGlobalFlags(fs), Subcommands: []*ffcli.Command{ commands.Version(), }, Exec: cfg.Exec, }, &cfg } // Exec prints help instructions, because the root `billman` // command has no functionality. func (c *Config) Exec(context.Context, []string) error { return flag.ErrHelp } // checkPeriod ensures that the period flag unit // is in days and that it is > 0 and < 31. func CheckPeriod(period string) error { suffix := "d" // 1d, 7d, 30d etc if !strings.HasSuffix(period, suffix) { fmt.Println("period must be in days. e.g.: 1d, 7d") return flag.ErrHelp } p := strings.TrimSuffix(period, suffix) days, err := strconv.Atoi(p) if err != nil { return flag.ErrHelp } if days <= 0 || days >= 31 { fmt.Println("period must be between 1-30d") return flag.ErrHelp } return nil } // AfterParse sets values for the global Config struct // based on flags passed in to the CLI. It runs before any // sub command Exec functions are invoked. func (c *Config) AfterParse() error { err := CheckRequiredFlags(GlobalFlags) if err != nil { return err } sqlConfig := &edgesql.SQLConfig{ Host: dbHost, User: dbUser, DbName: dbName, DbPassword: dbPassword, } db, err := sqlConfig.CheckConnection() if err != nil { fmt.Fprintf(os.Stderr, "error getting db connection: %v\n", err) } const noData = "unknown" edgeCluster, err := edgesql.ClusterByClusterID(db, clusterID) if err != nil { fmt.Fprintf(os.Stderr, "error getting clusters from the edge db: %v\n", err) edgeCluster.BannerEdgeID = noData edgeCluster.BannerName = noData edgeCluster.ClusterEdgeID = clusterID edgeCluster.ClusterName = noData edgeCluster.ProjectID = noData } c.Cluster = edgeCluster c.ClusterID = clusterID c.Period = period c.TopLevelProjectID = topLevelProjectID c.Options = costs.Options{ Output: output, ShowDisclaimer: showDisclaimer, ShowHeader: showHeader, } c.LoggingRate = loggingRate c.MetricsRate = metricsRate return nil }