mirror of
https://github.com/kemko/nomad.git
synced 2026-01-06 02:15:43 +03:00
Move the secure variables quota enforcement calls into the state store to ensure quota checks are atomic with quota updates (in the same transaction). Switch to a machine-size int instead of a uint64 for quota tracking. The ENT-side quota spec is described as int, and negative values have a meaning as "not permitted at all". Using the same type for tracking will make it easier to the math around checks, and uint64 is infeasibly large anyways. Add secure vars to quota HTTP API and CLI outputs and API docs.
251 lines
6.8 KiB
Go
251 lines
6.8 KiB
Go
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] <quota>
|
|
|
|
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)
|
|
|
|
return strings.TrimSpace(helpText)
|
|
}
|
|
|
|
func (c *QuotaStatusCommand) AutocompleteFlags() complete.Flags {
|
|
return c.Meta.AutocompleteFlags(FlagSetClient)
|
|
}
|
|
|
|
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 {
|
|
flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
|
|
flags.Usage = func() { c.Ui.Output(c.Help()) }
|
|
|
|
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: <quota>")
|
|
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
|
|
}
|
|
|
|
// 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|Network Usage|Secure 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
|
|
}
|
|
|
|
specBits := 0
|
|
if len(specLimit.RegionLimit.Networks) == 1 {
|
|
specBits = *specLimit.RegionLimit.Networks[0].MBits
|
|
}
|
|
|
|
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))
|
|
net := fmt.Sprintf("- / %s", formatQuotaLimitInt(&specBits))
|
|
|
|
vars := fmt.Sprintf("- / %s", formatQuotaLimitInt(specLimit.SecureVariablesLimit))
|
|
limits[i] = fmt.Sprintf("%s|%s|%s|%s|%s|%s", specLimit.Region, cpu, memory, memoryMax, net, 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))
|
|
|
|
net := fmt.Sprintf("- / %s", formatQuotaLimitInt(&specBits))
|
|
if len(used.RegionLimit.Networks) == 1 {
|
|
net = fmt.Sprintf("%d / %s", *used.RegionLimit.Networks[0].MBits, formatQuotaLimitInt(&specBits))
|
|
}
|
|
|
|
vars := fmt.Sprintf("%d / %s", orZero(used.SecureVariablesLimit), formatQuotaLimitInt(specLimit.SecureVariablesLimit))
|
|
limits[i] = fmt.Sprintf("%s|%s|%s|%s|%s|%s", specLimit.Region, cpu, memory, memoryMax, net, 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
|
|
}
|
|
}
|