// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package command import ( "encoding/base64" "fmt" "sort" "strconv" "strings" "github.com/hashicorp/nomad/api" "github.com/posener/complete" ) type QuotaStatusCommand struct { Meta } func (c *QuotaStatusCommand) Help() string { helpText := ` Usage: nomad quota status [options] Status is used to view the status of a particular quota specification. If ACLs are enabled, this command requires a token with the 'quota:read' capability and access to any namespaces that the quota is applied to. General Options: ` + generalOptionsUsage(usageOptsDefault) + ` Status Specific Options: -json Output the latest quota status information in a JSON format. -t Format and display quota status information using a Go template. ` return strings.TrimSpace(helpText) } func (c *QuotaStatusCommand) AutocompleteFlags() complete.Flags { return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient), complete.Flags{ "-json": complete.PredictNothing, "-t": complete.PredictAnything, }) } func (c *QuotaStatusCommand) AutocompleteArgs() complete.Predictor { return QuotaPredictor(c.Meta.Client) } func (c *QuotaStatusCommand) Synopsis() string { return "Display a quota's status and current usage" } func (c *QuotaStatusCommand) Name() string { return "quota status" } func (c *QuotaStatusCommand) Run(args []string) int { var json bool var tmpl string flags := c.Meta.FlagSet(c.Name(), FlagSetClient) flags.Usage = func() { c.Ui.Output(c.Help()) } flags.BoolVar(&json, "json", false, "") flags.StringVar(&tmpl, "t", "", "") if err := flags.Parse(args); err != nil { return 1 } // Check that we got one arguments args = flags.Args() if l := len(args); l != 1 { c.Ui.Error("This command takes one argument: ") c.Ui.Error(commandErrorText(c)) return 1 } name := args[0] // Get the HTTP client client, err := c.Meta.Client() if err != nil { c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err)) return 1 } // Do a prefix lookup quotas := client.Quotas() spec, possible, err := getQuota(quotas, name) if err != nil { c.Ui.Error(fmt.Sprintf("Error retrieving quota: %s", err)) return 1 } if len(possible) != 0 { c.Ui.Error(fmt.Sprintf("Prefix matched multiple quotas\n\n%s", formatQuotaSpecs(possible))) return 1 } if json || len(tmpl) > 0 { out, err := Format(json, tmpl, spec) if err != nil { c.Ui.Error(err.Error()) return 1 } c.Ui.Output(out) return 0 } // Format the basics c.Ui.Output(formatQuotaSpecBasics(spec)) // Get the quota usages usages, failures := quotaUsages(spec, quotas) // Format the limits c.Ui.Output(c.Colorize().Color("\n[bold]Quota Limits[reset]")) c.Ui.Output(formatQuotaLimits(spec, usages)) // Display any failures if len(failures) != 0 { c.Ui.Error(c.Colorize().Color("\n[bold][red]Lookup Failures[reset]")) for region, failure := range failures { c.Ui.Error(fmt.Sprintf(" * Failed to retrieve quota usage for region %q: %v", region, failure)) return 1 } } return 0 } // quotaUsages returns the quota usages for the limits described by the spec. It // will make a request to each referenced Nomad region. If the region couldn't // be contacted, the error will be stored in the failures map func quotaUsages(spec *api.QuotaSpec, client *api.Quotas) (usages map[string]*api.QuotaUsage, failures map[string]error) { // Determine the regions we have limits for regions := make(map[string]struct{}) for _, limit := range spec.Limits { regions[limit.Region] = struct{}{} } usages = make(map[string]*api.QuotaUsage, len(regions)) failures = make(map[string]error) q := api.QueryOptions{} // Retrieve the usage per region for region := range regions { q.Region = region usage, _, err := client.Usage(spec.Name, &q) if err != nil { failures[region] = err continue } usages[region] = usage } return usages, failures } // formatQuotaSpecBasics formats the basic information of the quota // specification. func formatQuotaSpecBasics(spec *api.QuotaSpec) string { basic := []string{ fmt.Sprintf("Name|%s", spec.Name), fmt.Sprintf("Description|%s", spec.Description), fmt.Sprintf("Limits|%d", len(spec.Limits)), } return formatKV(basic) } // formatQuotaLimits formats the limits to display the quota usage versus the // limit per quota limit. It takes as input the specification as well as quota // usage by region. The formatter handles missing usages. func formatQuotaLimits(spec *api.QuotaSpec, usages map[string]*api.QuotaUsage) string { if len(spec.Limits) == 0 { return "No quota limits defined" } // Sort the limits sort.Sort(api.QuotaLimitSort(spec.Limits)) limits := make([]string, len(spec.Limits)+1) limits[0] = "Region|CPU Usage|Memory Usage|Memory Max Usage|Variables Usage" i := 0 for _, specLimit := range spec.Limits { i++ // lookupUsage returns the regions quota usage for the limit lookupUsage := func() (*api.QuotaLimit, bool) { usage, ok := usages[specLimit.Region] if !ok { return nil, false } used, ok := usage.Used[base64.StdEncoding.EncodeToString(specLimit.Hash)] return used, ok } used, ok := lookupUsage() if !ok { cpu := fmt.Sprintf("- / %s", formatQuotaLimitInt(specLimit.RegionLimit.CPU)) memory := fmt.Sprintf("- / %s", formatQuotaLimitInt(specLimit.RegionLimit.MemoryMB)) memoryMax := fmt.Sprintf("- / %s", formatQuotaLimitInt(specLimit.RegionLimit.MemoryMaxMB)) vars := fmt.Sprintf("- / %s", formatQuotaLimitInt(specLimit.VariablesLimit)) limits[i] = fmt.Sprintf("%s|%s|%s|%s|%s", specLimit.Region, cpu, memory, memoryMax, vars) continue } orZero := func(v *int) int { if v == nil { return 0 } return *v } cpu := fmt.Sprintf("%d / %s", orZero(used.RegionLimit.CPU), formatQuotaLimitInt(specLimit.RegionLimit.CPU)) memory := fmt.Sprintf("%d / %s", orZero(used.RegionLimit.MemoryMB), formatQuotaLimitInt(specLimit.RegionLimit.MemoryMB)) memoryMax := fmt.Sprintf("%d / %s", orZero(used.RegionLimit.MemoryMaxMB), formatQuotaLimitInt(specLimit.RegionLimit.MemoryMaxMB)) vars := fmt.Sprintf("%d / %s", orZero(used.VariablesLimit), formatQuotaLimitInt(specLimit.VariablesLimit)) limits[i] = fmt.Sprintf("%s|%s|%s|%s|%s", specLimit.Region, cpu, memory, memoryMax, vars) } return formatList(limits) } // formatQuotaLimitInt takes a integer resource value and returns the // appropriate string for output. func formatQuotaLimitInt(value *int) string { if value == nil { return "-" } v := *value if v < 0 { return "0" } else if v == 0 { return "inf" } return strconv.Itoa(v) } func getQuota(client *api.Quotas, quota string) (match *api.QuotaSpec, possible []*api.QuotaSpec, err error) { // Do a prefix lookup quotas, _, err := client.PrefixList(quota, nil) if err != nil { return nil, nil, err } l := len(quotas) switch { case l == 0: return nil, nil, fmt.Errorf("Quota %q matched no quotas", quota) case l == 1: return quotas[0], nil, nil default: return nil, quotas, nil } }