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 }