...

Source file src/edge-infra.dev/pkg/edge/monitoring/billman/cmd/billman.go

Documentation: edge-infra.dev/pkg/edge/monitoring/billman/cmd

     1  // Package cmd contains the billman CLI
     2  package cmd
     3  
     4  import (
     5  	"context"
     6  	"flag"
     7  	"fmt"
     8  	"os"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"github.com/peterbourgon/ff/v3/ffcli"
    13  
    14  	"edge-infra.dev/pkg/edge/monitoring/billman/costs"
    15  	"edge-infra.dev/pkg/edge/monitoring/billman/edgesql"
    16  	"edge-infra.dev/pkg/lib/cli/commands"
    17  )
    18  
    19  const (
    20  	// TODO: see if the Google Cloud pricing api can be used instead.
    21  	// https://cloud.google.com/billing/v1/how-tos/catalog-api
    22  	loggingRate = .50  // $0.50/GiB for logs ingested
    23  	metricsRate = .060 //$0.060/million samples: first 0-50 billion samples
    24  	name        = "billman"
    25  )
    26  
    27  // Config for the root command, including flags and types that should be
    28  // available to each subcommand.
    29  type Config struct {
    30  	Cluster           edgesql.EdgeCluster
    31  	ClusterID         string
    32  	Period            string
    33  	TopLevelProjectID string
    34  	Options           costs.Options
    35  	LoggingRate       float64
    36  	MetricsRate       float64
    37  	DisplayType       string
    38  }
    39  
    40  var (
    41  	clusterID         string
    42  	period            string
    43  	topLevelProjectID string
    44  	output            string
    45  	dbHost            string
    46  	dbUser            string
    47  	dbName            string
    48  	dbPassword        string
    49  	showHeader        bool
    50  	showDisclaimer    bool
    51  )
    52  
    53  func SQLCreds(key string, defaultValue string) string {
    54  	value := os.Getenv(key)
    55  	if value == "" {
    56  		return defaultValue
    57  	}
    58  	return value
    59  }
    60  
    61  var GlobalFlags = func() *flag.FlagSet {
    62  	fset := flag.NewFlagSet("global", flag.ExitOnError)
    63  	fset.StringVar(&clusterID, "clusterID", "", "(required) Edge cluster ID")
    64  	fset.StringVar(&period, "period", "1d", "time window for the query. e.g.: 1d,7d")
    65  	fset.StringVar(&topLevelProjectID, "topLevelProjectID", "", "(required) project id for the foreman cluster")
    66  	fset.StringVar(&output, "output", "tab", "csv or tab output")
    67  	fset.StringVar(&dbHost, "dbHost", SQLCreds("SQL_CONNECTION_NAME", "unknown"), "(optional) SQL DB connection name")
    68  	fset.StringVar(&dbUser, "dbUser", SQLCreds("SQL_USER", "unknown"), "(optional) SQL DB user")
    69  	fset.StringVar(&dbName, "dbName", SQLCreds("SQL_DB_NAME", "unknown"), "(optional) SQL DB name")
    70  	fset.StringVar(&dbPassword, "dbPassword", SQLCreds("SQL_PASSWORD", "unknown"), "(optional) SQL DB password")
    71  	fset.BoolVar(&showHeader, "show-header", true, "if false don't print the table header")
    72  	fset.BoolVar(&showDisclaimer, "show-disclaimer", true, "if false don't print the disclaimer")
    73  	return fset
    74  }()
    75  
    76  func WithGlobalFlags(fset *flag.FlagSet) *flag.FlagSet {
    77  	GlobalFlags.VisitAll(func(f *flag.Flag) {
    78  		fset.Var(f.Value, f.Name, f.Usage)
    79  	})
    80  	return fset
    81  }
    82  
    83  func CheckRequiredFlags(flags *flag.FlagSet) error {
    84  	missingFlag := false
    85  	flags.VisitAll(func(f *flag.Flag) {
    86  		if strings.TrimSpace(f.Value.String()) == "" {
    87  			missingFlag = true
    88  		}
    89  	})
    90  	if missingFlag {
    91  		return flag.ErrHelp
    92  	}
    93  	return nil
    94  }
    95  
    96  func New() (*ffcli.Command, *Config) {
    97  	cfg := Config{}
    98  	fs := flag.NewFlagSet(name, flag.ExitOnError)
    99  	return &ffcli.Command{
   100  		Name:       name,
   101  		ShortUsage: name + " [subcommand] [flags]",
   102  		FlagSet:    WithGlobalFlags(fs),
   103  		Subcommands: []*ffcli.Command{
   104  			commands.Version(),
   105  		},
   106  		Exec: cfg.Exec,
   107  	}, &cfg
   108  }
   109  
   110  // Exec prints help instructions, because the root `billman`
   111  // command has no functionality.
   112  func (c *Config) Exec(context.Context, []string) error {
   113  	return flag.ErrHelp
   114  }
   115  
   116  // checkPeriod ensures that the period flag unit
   117  // is in days and that it is > 0 and < 31.
   118  func CheckPeriod(period string) error {
   119  	suffix := "d" // 1d, 7d, 30d etc
   120  	if !strings.HasSuffix(period, suffix) {
   121  		fmt.Println("period must be in days. e.g.: 1d, 7d")
   122  		return flag.ErrHelp
   123  	}
   124  
   125  	p := strings.TrimSuffix(period, suffix)
   126  	days, err := strconv.Atoi(p)
   127  	if err != nil {
   128  		return flag.ErrHelp
   129  	}
   130  
   131  	if days <= 0 || days >= 31 {
   132  		fmt.Println("period must be between 1-30d")
   133  		return flag.ErrHelp
   134  	}
   135  
   136  	return nil
   137  }
   138  
   139  // AfterParse sets values for the global Config struct
   140  // based on flags passed in to the CLI.  It runs before any
   141  // sub command Exec functions are invoked.
   142  func (c *Config) AfterParse() error {
   143  	err := CheckRequiredFlags(GlobalFlags)
   144  	if err != nil {
   145  		return err
   146  	}
   147  
   148  	sqlConfig := &edgesql.SQLConfig{
   149  		Host:       dbHost,
   150  		User:       dbUser,
   151  		DbName:     dbName,
   152  		DbPassword: dbPassword,
   153  	}
   154  
   155  	db, err := sqlConfig.CheckConnection()
   156  	if err != nil {
   157  		fmt.Fprintf(os.Stderr, "error getting db connection: %v\n", err)
   158  	}
   159  
   160  	const noData = "unknown"
   161  	edgeCluster, err := edgesql.ClusterByClusterID(db, clusterID)
   162  	if err != nil {
   163  		fmt.Fprintf(os.Stderr, "error getting clusters from the edge db: %v\n", err)
   164  		edgeCluster.BannerEdgeID = noData
   165  		edgeCluster.BannerName = noData
   166  		edgeCluster.ClusterEdgeID = clusterID
   167  		edgeCluster.ClusterName = noData
   168  		edgeCluster.ProjectID = noData
   169  	}
   170  
   171  	c.Cluster = edgeCluster
   172  	c.ClusterID = clusterID
   173  	c.Period = period
   174  	c.TopLevelProjectID = topLevelProjectID
   175  	c.Options = costs.Options{
   176  		Output:         output,
   177  		ShowDisclaimer: showDisclaimer,
   178  		ShowHeader:     showHeader,
   179  	}
   180  	c.LoggingRate = loggingRate
   181  	c.MetricsRate = metricsRate
   182  	return nil
   183  }
   184  

View as plain text