// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package instances import ( "bytes" "flag" "fmt" "sort" "strings" "text/tabwriter" "github.com/mitchellh/cli" "golang.org/x/exp/maps" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/command/flags" ) func New(ui cli.Ui) *cmd { c := &cmd{UI: ui} c.init() return c } type cmd struct { UI cli.Ui flags *flag.FlagSet http *flags.HTTPFlags help string // flags onlyBillable bool onlyConnect bool allDatacenters bool } func (c *cmd) init() { c.flags = flag.NewFlagSet("", flag.ContinueOnError) c.flags.BoolVar(&c.onlyBillable, "billable", false, "Display only billable service info. "+ "Cannot be used with -connect.") c.flags.BoolVar(&c.onlyConnect, "connect", false, "Display only Connect service info."+ "Cannot be used with -billable.") c.flags.BoolVar(&c.allDatacenters, "all-datacenters", false, "Display service counts from "+ "all datacenters.") c.http = &flags.HTTPFlags{} flags.Merge(c.flags, c.http.ClientFlags()) flags.Merge(c.flags, c.http.ServerFlags()) c.help = flags.Usage(help, c.flags) } func (c *cmd) Run(args []string) int { if err := c.flags.Parse(args); err != nil { return 1 } if l := len(c.flags.Args()); l > 0 { c.UI.Error(fmt.Sprintf("Too many arguments (expected 0, got %d)", l)) return 1 } if c.onlyBillable && c.onlyConnect { c.UI.Error("Cannot specify both -billable and -connect flags") return 1 } // Create and test the HTTP client client, err := c.http.APIClient() if err != nil { c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err)) return 1 } billableTotal := 0 var datacenterBillableTotals []string usage, _, err := client.Operator().Usage(&api.QueryOptions{Global: c.allDatacenters}) if err != nil { c.UI.Error(fmt.Sprintf("Error fetching usage information: %s", err)) return 1 } for dc, usage := range usage.Usage { billableTotal += usage.BillableServiceInstances datacenterBillableTotals = append(datacenterBillableTotals, fmt.Sprintf("%s Billable Service Instances: %d", dc, usage.BillableServiceInstances)) } // Output billable service counts if !c.onlyConnect { c.UI.Output(fmt.Sprintf("Billable Service Instances Total: %d", billableTotal)) sort.Strings(datacenterBillableTotals) for _, datacenterTotal := range datacenterBillableTotals { c.UI.Output(datacenterTotal) } c.UI.Output("\nBillable Services") billableOutput, err := formatServiceCounts(usage.Usage, true, c.allDatacenters) if err != nil { c.UI.Error(err.Error()) return 1 } c.UI.Output(billableOutput + "\n") c.UI.Output("\nNodes") nodesOutput, err := formatNodesCounts(usage.Usage) if err != nil { c.UI.Error(err.Error()) return 1 } c.UI.Output(nodesOutput + "\n\n") } // Output Connect service counts if !c.onlyBillable { c.UI.Output("Connect Services") connectOutput, err := formatServiceCounts(usage.Usage, false, c.allDatacenters) if err != nil { c.UI.Error(err.Error()) return 1 } c.UI.Output(connectOutput) } return 0 } func formatNodesCounts(usageStats map[string]api.ServiceUsage) (string, error) { var output bytes.Buffer tw := tabwriter.NewWriter(&output, 0, 2, 6, ' ', 0) nodesTotal := 0 fmt.Fprintf(tw, "Datacenter\t") fmt.Fprintf(tw, "Count\t") fmt.Fprint(tw, "\t\n") nodes := maps.Keys(usageStats) sort.Strings(nodes) for _, dc := range nodes { nodesTotal += usageStats[dc].Nodes fmt.Fprintf(tw, "%s\t%d\n", dc, usageStats[dc].Nodes) } fmt.Fprint(tw, "\t\n") fmt.Fprintf(tw, "Total") fmt.Fprintf(tw, "\t%d", nodesTotal) if err := tw.Flush(); err != nil { return "", fmt.Errorf("Error flushing tabwriter: %s", err) } return strings.TrimSpace(output.String()), nil } func formatServiceCounts(usageStats map[string]api.ServiceUsage, billable, showDatacenter bool) (string, error) { var output bytes.Buffer tw := tabwriter.NewWriter(&output, 0, 2, 6, ' ', 0) var serviceCounts []serviceCount for datacenter, usage := range usageStats { if billable { serviceCounts = append(serviceCounts, getBillableInstanceCounts(usage, datacenter)...) } else { serviceCounts = append(serviceCounts, getConnectInstanceCounts(usage, datacenter)...) } } sortServiceCounts(serviceCounts) if showDatacenter { fmt.Fprintf(tw, "Datacenter\t") } if showPartitionNamespace { fmt.Fprintf(tw, "Partition\tNamespace\t") } if !billable { fmt.Fprintf(tw, "Type\t") } else { fmt.Fprintf(tw, "Services\t") } fmt.Fprintf(tw, "Service instances\n") serviceTotal := 0 instanceTotal := 0 for _, c := range serviceCounts { if showDatacenter { fmt.Fprintf(tw, "%s\t", c.datacenter) } if showPartitionNamespace { fmt.Fprintf(tw, "%s\t%s\t", c.partition, c.namespace) } if !billable { fmt.Fprintf(tw, "%s\t", c.serviceType) } else { fmt.Fprintf(tw, "%d\t", c.services) } fmt.Fprintf(tw, "%d\n", c.instanceCount) serviceTotal += c.services instanceTotal += c.instanceCount } // Show total counts if there's multiple rows because of datacenter or partition/ns view if showDatacenter || showPartitionNamespace { if showDatacenter { fmt.Fprint(tw, "\t") } if showPartitionNamespace { fmt.Fprint(tw, "\t\t") } fmt.Fprint(tw, "\t\n") fmt.Fprintf(tw, "Total") if showPartitionNamespace { fmt.Fprint(tw, "\t") if showDatacenter { fmt.Fprint(tw, "\t") } } if billable { fmt.Fprintf(tw, "\t%d\t%d\n", serviceTotal, instanceTotal) } else { fmt.Fprintf(tw, "\t\t%d\n", instanceTotal) } } if err := tw.Flush(); err != nil { return "", fmt.Errorf("Error flushing tabwriter: %s", err) } return strings.TrimSpace(output.String()), nil } type serviceCount struct { datacenter string partition string namespace string serviceType string instanceCount int services int } // Sort entries by datacenter > partition > namespace func sortServiceCounts(counts []serviceCount) { sort.Slice(counts, func(i, j int) bool { if counts[i].datacenter != counts[j].datacenter { return counts[i].datacenter < counts[j].datacenter } if counts[i].partition != counts[j].partition { return counts[i].partition < counts[j].partition } if counts[i].namespace != counts[j].namespace { return counts[i].namespace < counts[j].namespace } return counts[i].serviceType < counts[j].serviceType }) } func (c *cmd) Synopsis() string { return synopsis } func (c *cmd) Help() string { return c.help } const ( synopsis = "Display service instance usage information" help = ` Usage: consul operator usage instances [options] Retrieves usage information about the number of services registered in a given datacenter. By default, the datacenter of the local agent is queried. To retrieve the service usage data: $ consul operator usage instances To show only billable service instance counts: $ consul operator usage instances -billable To show only connect service instance counts: $ consul operator usage instances -connect For a full list of options and examples, please see the Consul documentation. ` )