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  }