github.com/hernad/nomad@v1.6.112/command/quota_apply.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package command 5 6 import ( 7 "bytes" 8 "encoding/json" 9 "fmt" 10 "io" 11 "os" 12 "strings" 13 14 multierror "github.com/hashicorp/go-multierror" 15 "github.com/hashicorp/hcl" 16 "github.com/hashicorp/hcl/hcl/ast" 17 "github.com/hernad/nomad/api" 18 "github.com/hernad/nomad/helper" 19 "github.com/mitchellh/mapstructure" 20 "github.com/posener/complete" 21 ) 22 23 type QuotaApplyCommand struct { 24 Meta 25 } 26 27 func (c *QuotaApplyCommand) Help() string { 28 helpText := ` 29 Usage: nomad quota apply [options] <input> 30 31 Apply is used to create or update a quota specification. The specification file 32 will be read from stdin by specifying "-", otherwise a path to the file is 33 expected. 34 35 If ACLs are enabled, this command requires a token with the 'quota:write' 36 capability. 37 38 General Options: 39 40 ` + generalOptionsUsage(usageOptsDefault) + ` 41 42 Apply Options: 43 44 -json 45 Parse the input as a JSON quota specification. 46 ` 47 48 return strings.TrimSpace(helpText) 49 } 50 51 func (c *QuotaApplyCommand) AutocompleteFlags() complete.Flags { 52 return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient), 53 complete.Flags{ 54 "-json": complete.PredictNothing, 55 }) 56 } 57 58 func (c *QuotaApplyCommand) AutocompleteArgs() complete.Predictor { 59 return complete.PredictFiles("*") 60 } 61 62 func (c *QuotaApplyCommand) Synopsis() string { 63 return "Create or update a quota specification" 64 } 65 66 func (c *QuotaApplyCommand) Name() string { return "quota apply" } 67 68 func (c *QuotaApplyCommand) Run(args []string) int { 69 var jsonInput bool 70 flags := c.Meta.FlagSet(c.Name(), FlagSetClient) 71 flags.Usage = func() { c.Ui.Output(c.Help()) } 72 flags.BoolVar(&jsonInput, "json", false, "") 73 74 if err := flags.Parse(args); err != nil { 75 return 1 76 } 77 78 // Check that we get exactly one argument 79 args = flags.Args() 80 if l := len(args); l != 1 { 81 c.Ui.Error("This command takes one argument: <input>") 82 c.Ui.Error(commandErrorText(c)) 83 return 1 84 } 85 86 // Read the file contents 87 file := args[0] 88 var rawQuota []byte 89 var err error 90 if file == "-" { 91 rawQuota, err = io.ReadAll(os.Stdin) 92 if err != nil { 93 c.Ui.Error(fmt.Sprintf("Failed to read stdin: %v", err)) 94 return 1 95 } 96 } else { 97 rawQuota, err = os.ReadFile(file) 98 if err != nil { 99 c.Ui.Error(fmt.Sprintf("Failed to read file: %v", err)) 100 return 1 101 } 102 } 103 104 var spec *api.QuotaSpec 105 if jsonInput { 106 var jsonSpec api.QuotaSpec 107 dec := json.NewDecoder(bytes.NewBuffer(rawQuota)) 108 if err := dec.Decode(&jsonSpec); err != nil { 109 c.Ui.Error(fmt.Sprintf("Failed to parse quota: %v", err)) 110 return 1 111 } 112 spec = &jsonSpec 113 } else { 114 hclSpec, err := parseQuotaSpec(rawQuota) 115 if err != nil { 116 c.Ui.Error(fmt.Sprintf("Error parsing quota specification: %s", err)) 117 return 1 118 } 119 120 spec = hclSpec 121 } 122 123 // Get the HTTP client 124 client, err := c.Meta.Client() 125 if err != nil { 126 c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err)) 127 return 1 128 } 129 130 _, err = client.Quotas().Register(spec, nil) 131 if err != nil { 132 c.Ui.Error(fmt.Sprintf("Error applying quota specification: %s", err)) 133 return 1 134 } 135 136 c.Ui.Output(fmt.Sprintf("Successfully applied quota specification %q!", spec.Name)) 137 return 0 138 } 139 140 // parseQuotaSpec is used to parse the quota specification from HCL 141 func parseQuotaSpec(input []byte) (*api.QuotaSpec, error) { 142 root, err := hcl.ParseBytes(input) 143 if err != nil { 144 return nil, err 145 } 146 147 // Top-level item should be a list 148 list, ok := root.Node.(*ast.ObjectList) 149 if !ok { 150 return nil, fmt.Errorf("error parsing: root should be an object") 151 } 152 153 var spec api.QuotaSpec 154 if err := parseQuotaSpecImpl(&spec, list); err != nil { 155 return nil, err 156 } 157 158 return &spec, nil 159 } 160 161 // parseQuotaSpecImpl parses the quota spec taking as input the AST tree 162 func parseQuotaSpecImpl(result *api.QuotaSpec, list *ast.ObjectList) error { 163 // Check for invalid keys 164 valid := []string{ 165 "name", 166 "description", 167 "limit", 168 } 169 if err := helper.CheckHCLKeys(list, valid); err != nil { 170 return err 171 } 172 173 // Decode the full thing into a map[string]interface for ease 174 var m map[string]interface{} 175 if err := hcl.DecodeObject(&m, list); err != nil { 176 return err 177 } 178 179 // Manually parse 180 delete(m, "limit") 181 182 // Decode the rest 183 if err := mapstructure.WeakDecode(m, result); err != nil { 184 return err 185 } 186 187 // Parse limits 188 if o := list.Filter("limit"); len(o.Items) > 0 { 189 if err := parseQuotaLimits(&result.Limits, o); err != nil { 190 return multierror.Prefix(err, "limit ->") 191 } 192 } 193 194 return nil 195 } 196 197 // parseQuotaLimits parses the quota limits 198 func parseQuotaLimits(result *[]*api.QuotaLimit, list *ast.ObjectList) error { 199 for _, o := range list.Elem().Items { 200 // Check for invalid keys 201 valid := []string{ 202 "region", 203 "region_limit", 204 "variables_limit", 205 } 206 if err := helper.CheckHCLKeys(o.Val, valid); err != nil { 207 return err 208 } 209 210 var m map[string]interface{} 211 if err := hcl.DecodeObject(&m, o.Val); err != nil { 212 return err 213 } 214 215 // Manually parse 216 delete(m, "region_limit") 217 218 // Decode the rest 219 var limit api.QuotaLimit 220 if err := mapstructure.WeakDecode(m, &limit); err != nil { 221 return err 222 } 223 224 // We need this later 225 var listVal *ast.ObjectList 226 if ot, ok := o.Val.(*ast.ObjectType); ok { 227 listVal = ot.List 228 } else { 229 return fmt.Errorf("limit should be an object") 230 } 231 232 // Parse limits 233 if o := listVal.Filter("region_limit"); len(o.Items) > 0 { 234 limit.RegionLimit = new(api.Resources) 235 if err := parseQuotaResource(limit.RegionLimit, o); err != nil { 236 return multierror.Prefix(err, "region_limit ->") 237 } 238 } 239 240 *result = append(*result, &limit) 241 } 242 243 return nil 244 } 245 246 // parseQuotaResource parses the region_limit resources 247 func parseQuotaResource(result *api.Resources, list *ast.ObjectList) error { 248 list = list.Elem() 249 if len(list.Items) == 0 { 250 return nil 251 } 252 if len(list.Items) > 1 { 253 return fmt.Errorf("only one 'region_limit' block allowed per limit") 254 } 255 256 // Get our resource object 257 o := list.Items[0] 258 259 // We need this later 260 var listVal *ast.ObjectList 261 if ot, ok := o.Val.(*ast.ObjectType); ok { 262 listVal = ot.List 263 } else { 264 return fmt.Errorf("resource: should be an object") 265 } 266 267 // Check for invalid keys 268 valid := []string{ 269 "cpu", 270 "memory", 271 "memory_max", 272 } 273 if err := helper.CheckHCLKeys(listVal, valid); err != nil { 274 return multierror.Prefix(err, "resources ->") 275 } 276 277 var m map[string]interface{} 278 if err := hcl.DecodeObject(&m, o.Val); err != nil { 279 return err 280 } 281 282 if err := mapstructure.WeakDecode(m, result); err != nil { 283 return err 284 } 285 286 return nil 287 }