github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/command/quota_status.go (about)

     1  package command
     2  
     3  import (
     4  	"encoding/base64"
     5  	"fmt"
     6  	"sort"
     7  	"strconv"
     8  	"strings"
     9  
    10  	"github.com/hashicorp/nomad/api"
    11  	"github.com/posener/complete"
    12  )
    13  
    14  type QuotaStatusCommand struct {
    15  	Meta
    16  }
    17  
    18  func (c *QuotaStatusCommand) Help() string {
    19  	helpText := `
    20  Usage: nomad quota status [options] <quota>
    21  
    22    Status is used to view the status of a particular quota specification.
    23  
    24    If ACLs are enabled, this command requires a token with the 'quota:read'
    25    capability and access to any namespaces that the quota is applied to.
    26  
    27  General Options:
    28  
    29    ` + generalOptionsUsage(usageOptsDefault)
    30  
    31  	return strings.TrimSpace(helpText)
    32  }
    33  
    34  func (c *QuotaStatusCommand) AutocompleteFlags() complete.Flags {
    35  	return c.Meta.AutocompleteFlags(FlagSetClient)
    36  }
    37  
    38  func (c *QuotaStatusCommand) AutocompleteArgs() complete.Predictor {
    39  	return QuotaPredictor(c.Meta.Client)
    40  }
    41  
    42  func (c *QuotaStatusCommand) Synopsis() string {
    43  	return "Display a quota's status and current usage"
    44  }
    45  
    46  func (c *QuotaStatusCommand) Name() string { return "quota status" }
    47  
    48  func (c *QuotaStatusCommand) Run(args []string) int {
    49  	flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
    50  	flags.Usage = func() { c.Ui.Output(c.Help()) }
    51  
    52  	if err := flags.Parse(args); err != nil {
    53  		return 1
    54  	}
    55  
    56  	// Check that we got one arguments
    57  	args = flags.Args()
    58  	if l := len(args); l != 1 {
    59  		c.Ui.Error("This command takes one argument: <quota>")
    60  		c.Ui.Error(commandErrorText(c))
    61  		return 1
    62  	}
    63  
    64  	name := args[0]
    65  
    66  	// Get the HTTP client
    67  	client, err := c.Meta.Client()
    68  	if err != nil {
    69  		c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
    70  		return 1
    71  	}
    72  
    73  	// Do a prefix lookup
    74  	quotas := client.Quotas()
    75  	spec, possible, err := getQuota(quotas, name)
    76  	if err != nil {
    77  		c.Ui.Error(fmt.Sprintf("Error retrieving quota: %s", err))
    78  		return 1
    79  	}
    80  
    81  	if len(possible) != 0 {
    82  		c.Ui.Error(fmt.Sprintf("Prefix matched multiple quotas\n\n%s", formatQuotaSpecs(possible)))
    83  		return 1
    84  	}
    85  
    86  	// Format the basics
    87  	c.Ui.Output(formatQuotaSpecBasics(spec))
    88  
    89  	// Get the quota usages
    90  	usages, failures := quotaUsages(spec, quotas)
    91  
    92  	// Format the limits
    93  	c.Ui.Output(c.Colorize().Color("\n[bold]Quota Limits[reset]"))
    94  	c.Ui.Output(formatQuotaLimits(spec, usages))
    95  
    96  	// Display any failures
    97  	if len(failures) != 0 {
    98  		c.Ui.Error(c.Colorize().Color("\n[bold][red]Lookup Failures[reset]"))
    99  		for region, failure := range failures {
   100  			c.Ui.Error(fmt.Sprintf("  * Failed to retrieve quota usage for region %q: %v", region, failure))
   101  			return 1
   102  		}
   103  	}
   104  
   105  	return 0
   106  }
   107  
   108  // quotaUsages returns the quota usages for the limits described by the spec. It
   109  // will make a request to each referenced Nomad region. If the region couldn't
   110  // be contacted, the error will be stored in the failures map
   111  func quotaUsages(spec *api.QuotaSpec, client *api.Quotas) (usages map[string]*api.QuotaUsage, failures map[string]error) {
   112  	// Determine the regions we have limits for
   113  	regions := make(map[string]struct{})
   114  	for _, limit := range spec.Limits {
   115  		regions[limit.Region] = struct{}{}
   116  	}
   117  
   118  	usages = make(map[string]*api.QuotaUsage, len(regions))
   119  	failures = make(map[string]error)
   120  	q := api.QueryOptions{}
   121  
   122  	// Retrieve the usage per region
   123  	for region := range regions {
   124  		q.Region = region
   125  		usage, _, err := client.Usage(spec.Name, &q)
   126  		if err != nil {
   127  			failures[region] = err
   128  			continue
   129  		}
   130  
   131  		usages[region] = usage
   132  	}
   133  
   134  	return usages, failures
   135  }
   136  
   137  // formatQuotaSpecBasics formats the basic information of the quota
   138  // specification.
   139  func formatQuotaSpecBasics(spec *api.QuotaSpec) string {
   140  	basic := []string{
   141  		fmt.Sprintf("Name|%s", spec.Name),
   142  		fmt.Sprintf("Description|%s", spec.Description),
   143  		fmt.Sprintf("Limits|%d", len(spec.Limits)),
   144  	}
   145  
   146  	return formatKV(basic)
   147  }
   148  
   149  // formatQuotaLimits formats the limits to display the quota usage versus the
   150  // limit per quota limit. It takes as input the specification as well as quota
   151  // usage by region. The formatter handles missing usages.
   152  func formatQuotaLimits(spec *api.QuotaSpec, usages map[string]*api.QuotaUsage) string {
   153  	if len(spec.Limits) == 0 {
   154  		return "No quota limits defined"
   155  	}
   156  
   157  	// Sort the limits
   158  	sort.Sort(api.QuotaLimitSort(spec.Limits))
   159  
   160  	limits := make([]string, len(spec.Limits)+1)
   161  	limits[0] = "Region|CPU Usage|Memory Usage|Memory Max Usage|Variables Usage"
   162  	i := 0
   163  	for _, specLimit := range spec.Limits {
   164  		i++
   165  
   166  		// lookupUsage returns the regions quota usage for the limit
   167  		lookupUsage := func() (*api.QuotaLimit, bool) {
   168  			usage, ok := usages[specLimit.Region]
   169  			if !ok {
   170  				return nil, false
   171  			}
   172  
   173  			used, ok := usage.Used[base64.StdEncoding.EncodeToString(specLimit.Hash)]
   174  			return used, ok
   175  		}
   176  
   177  		used, ok := lookupUsage()
   178  		if !ok {
   179  			cpu := fmt.Sprintf("- / %s", formatQuotaLimitInt(specLimit.RegionLimit.CPU))
   180  			memory := fmt.Sprintf("- / %s", formatQuotaLimitInt(specLimit.RegionLimit.MemoryMB))
   181  			memoryMax := fmt.Sprintf("- / %s", formatQuotaLimitInt(specLimit.RegionLimit.MemoryMaxMB))
   182  
   183  			vars := fmt.Sprintf("- / %s", formatQuotaLimitInt(specLimit.VariablesLimit))
   184  			limits[i] = fmt.Sprintf("%s|%s|%s|%s|%s", specLimit.Region, cpu, memory, memoryMax, vars)
   185  			continue
   186  		}
   187  
   188  		orZero := func(v *int) int {
   189  			if v == nil {
   190  				return 0
   191  			}
   192  			return *v
   193  		}
   194  
   195  		cpu := fmt.Sprintf("%d / %s", orZero(used.RegionLimit.CPU), formatQuotaLimitInt(specLimit.RegionLimit.CPU))
   196  		memory := fmt.Sprintf("%d / %s", orZero(used.RegionLimit.MemoryMB), formatQuotaLimitInt(specLimit.RegionLimit.MemoryMB))
   197  		memoryMax := fmt.Sprintf("%d / %s", orZero(used.RegionLimit.MemoryMaxMB), formatQuotaLimitInt(specLimit.RegionLimit.MemoryMaxMB))
   198  
   199  		vars := fmt.Sprintf("%d / %s", orZero(used.VariablesLimit), formatQuotaLimitInt(specLimit.VariablesLimit))
   200  		limits[i] = fmt.Sprintf("%s|%s|%s|%s|%s", specLimit.Region, cpu, memory, memoryMax, vars)
   201  	}
   202  
   203  	return formatList(limits)
   204  }
   205  
   206  // formatQuotaLimitInt takes a integer resource value and returns the
   207  // appropriate string for output.
   208  func formatQuotaLimitInt(value *int) string {
   209  	if value == nil {
   210  		return "-"
   211  	}
   212  
   213  	v := *value
   214  	if v < 0 {
   215  		return "0"
   216  	} else if v == 0 {
   217  		return "inf"
   218  	}
   219  
   220  	return strconv.Itoa(v)
   221  }
   222  
   223  func getQuota(client *api.Quotas, quota string) (match *api.QuotaSpec, possible []*api.QuotaSpec, err error) {
   224  	// Do a prefix lookup
   225  	quotas, _, err := client.PrefixList(quota, nil)
   226  	if err != nil {
   227  		return nil, nil, err
   228  	}
   229  
   230  	l := len(quotas)
   231  	switch {
   232  	case l == 0:
   233  		return nil, nil, fmt.Errorf("Quota %q matched no quotas", quota)
   234  	case l == 1:
   235  		return quotas[0], nil, nil
   236  	default:
   237  		return nil, quotas, nil
   238  	}
   239  }