github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/command/quota_apply.go (about)

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