github.com/hernad/nomad@v1.6.112/command/namespace_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  	"github.com/hashicorp/hcl"
    15  	"github.com/hashicorp/hcl/hcl/ast"
    16  	"github.com/hernad/nomad/api"
    17  	flaghelper "github.com/hernad/nomad/helper/flags"
    18  	"github.com/mitchellh/mapstructure"
    19  	"github.com/posener/complete"
    20  )
    21  
    22  type NamespaceApplyCommand struct {
    23  	Meta
    24  }
    25  
    26  func (c *NamespaceApplyCommand) Help() string {
    27  	helpText := `
    28  Usage: nomad namespace apply [options] <input>
    29  
    30    Apply is used to create or update a namespace. The specification file
    31    will be read from stdin by specifying "-", otherwise a path to the file is
    32    expected.
    33  
    34    Instead of a file, you may instead pass the namespace name to create
    35    or update as the only argument.
    36  
    37    If ACLs are enabled, this command requires a management ACL token. In
    38    federated clusters, the namespace will be created in the authoritative region
    39    and will be replicated to all federated regions.
    40  
    41  General Options:
    42  
    43    ` + generalOptionsUsage(usageOptsDefault|usageOptsNoNamespace) + `
    44  
    45  Apply Options:
    46  
    47    -quota
    48      The quota to attach to the namespace.
    49  
    50    -description
    51      An optional description for the namespace.
    52  
    53    -json
    54      Parse the input as a JSON namespace specification.
    55  `
    56  	return strings.TrimSpace(helpText)
    57  }
    58  
    59  func (c *NamespaceApplyCommand) AutocompleteFlags() complete.Flags {
    60  	return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
    61  		complete.Flags{
    62  			"-description": complete.PredictAnything,
    63  			"-quota":       QuotaPredictor(c.Meta.Client),
    64  			"-json":        complete.PredictNothing,
    65  		})
    66  }
    67  
    68  func (c *NamespaceApplyCommand) AutocompleteArgs() complete.Predictor {
    69  	return complete.PredictOr(
    70  		NamespacePredictor(c.Meta.Client, nil),
    71  		complete.PredictFiles("*.hcl"),
    72  		complete.PredictFiles("*.json"),
    73  	)
    74  }
    75  
    76  func (c *NamespaceApplyCommand) Synopsis() string {
    77  	return "Create or update a namespace"
    78  }
    79  
    80  func (c *NamespaceApplyCommand) Name() string { return "namespace apply" }
    81  
    82  func (c *NamespaceApplyCommand) Run(args []string) int {
    83  	var jsonInput bool
    84  	var description, quota *string
    85  
    86  	flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
    87  	flags.Usage = func() { c.Ui.Output(c.Help()) }
    88  	flags.Var((flaghelper.FuncVar)(func(s string) error {
    89  		description = &s
    90  		return nil
    91  	}), "description", "")
    92  	flags.Var((flaghelper.FuncVar)(func(s string) error {
    93  		quota = &s
    94  		return nil
    95  	}), "quota", "")
    96  	flags.BoolVar(&jsonInput, "json", false, "")
    97  
    98  	if err := flags.Parse(args); err != nil {
    99  		return 1
   100  	}
   101  
   102  	// Check that we get exactly one argument
   103  	args = flags.Args()
   104  	if l := len(args); l != 1 {
   105  		c.Ui.Error("This command takes one argument: <input>")
   106  		c.Ui.Error(commandErrorText(c))
   107  		return 1
   108  	}
   109  
   110  	file := args[0]
   111  	var rawNamespace []byte
   112  	var err error
   113  	var namespace *api.Namespace
   114  
   115  	// Get the HTTP client
   116  	client, err := c.Meta.Client()
   117  	if err != nil {
   118  		c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
   119  		return 1
   120  	}
   121  
   122  	if fi, err := os.Stat(file); (file == "-" || err == nil) && !fi.IsDir() {
   123  		if quota != nil || description != nil {
   124  			c.Ui.Warn("Flags are ignored when a file is specified!")
   125  		}
   126  
   127  		if file == "-" {
   128  			rawNamespace, err = io.ReadAll(os.Stdin)
   129  			if err != nil {
   130  				c.Ui.Error(fmt.Sprintf("Failed to read stdin: %v", err))
   131  				return 1
   132  			}
   133  		} else {
   134  			rawNamespace, err = os.ReadFile(file)
   135  			if err != nil {
   136  				c.Ui.Error(fmt.Sprintf("Failed to read file: %v", err))
   137  				return 1
   138  			}
   139  		}
   140  		if jsonInput {
   141  			var jsonSpec api.Namespace
   142  			dec := json.NewDecoder(bytes.NewBuffer(rawNamespace))
   143  			if err := dec.Decode(&jsonSpec); err != nil {
   144  				c.Ui.Error(fmt.Sprintf("Failed to parse quota: %v", err))
   145  				return 1
   146  			}
   147  			namespace = &jsonSpec
   148  		} else {
   149  			hclSpec, err := parseNamespaceSpec(rawNamespace)
   150  			if err != nil {
   151  				c.Ui.Error(fmt.Sprintf("Error parsing quota specification: %s", err))
   152  				return 1
   153  			}
   154  
   155  			namespace = hclSpec
   156  		}
   157  	} else {
   158  		name := args[0]
   159  
   160  		// Validate we have at-least a name
   161  		if name == "" {
   162  			c.Ui.Error("Namespace name required")
   163  			return 1
   164  		}
   165  
   166  		// Lookup the given namespace
   167  		namespace, _, err = client.Namespaces().Info(name, nil)
   168  		if err != nil && !strings.Contains(err.Error(), "404") {
   169  			c.Ui.Error(fmt.Sprintf("Error looking up namespace: %s", err))
   170  			return 1
   171  		}
   172  
   173  		if namespace == nil {
   174  			namespace = &api.Namespace{
   175  				Name: name,
   176  			}
   177  		}
   178  
   179  		// Add what is set
   180  		if description != nil {
   181  			namespace.Description = *description
   182  		}
   183  		if quota != nil {
   184  			namespace.Quota = *quota
   185  		}
   186  	}
   187  	_, err = client.Namespaces().Register(namespace, nil)
   188  	if err != nil {
   189  		c.Ui.Error(fmt.Sprintf("Error applying namespace: %s", err))
   190  		return 1
   191  	}
   192  
   193  	c.Ui.Output(fmt.Sprintf("Successfully applied namespace %q!", namespace.Name))
   194  
   195  	return 0
   196  }
   197  
   198  // parseNamespaceSpec is used to parse the namespace specification from HCL
   199  func parseNamespaceSpec(input []byte) (*api.Namespace, error) {
   200  	root, err := hcl.ParseBytes(input)
   201  	if err != nil {
   202  		return nil, err
   203  	}
   204  
   205  	// Top-level item should be a list
   206  	list, ok := root.Node.(*ast.ObjectList)
   207  	if !ok {
   208  		return nil, fmt.Errorf("error parsing: root should be an object")
   209  	}
   210  
   211  	var spec api.Namespace
   212  	if err := parseNamespaceSpecImpl(&spec, list); err != nil {
   213  		return nil, err
   214  	}
   215  
   216  	return &spec, nil
   217  }
   218  
   219  // parseNamespaceSpec parses the quota namespace taking as input the AST tree
   220  func parseNamespaceSpecImpl(result *api.Namespace, list *ast.ObjectList) error {
   221  	// Decode the full thing into a map[string]interface for ease
   222  	var m map[string]interface{}
   223  	if err := hcl.DecodeObject(&m, list); err != nil {
   224  		return err
   225  	}
   226  
   227  	delete(m, "capabilities")
   228  	delete(m, "meta")
   229  	delete(m, "node_pool_config")
   230  
   231  	// Decode the rest
   232  	if err := mapstructure.WeakDecode(m, result); err != nil {
   233  		return err
   234  	}
   235  
   236  	cObj := list.Filter("capabilities")
   237  	if len(cObj.Items) > 0 {
   238  		for _, o := range cObj.Elem().Items {
   239  			ot, ok := o.Val.(*ast.ObjectType)
   240  			if !ok {
   241  				break
   242  			}
   243  			var opts *api.NamespaceCapabilities
   244  			if err := hcl.DecodeObject(&opts, ot.List); err != nil {
   245  				return err
   246  			}
   247  			result.Capabilities = opts
   248  			break
   249  		}
   250  	}
   251  
   252  	npObj := list.Filter("node_pool_config")
   253  	if len(npObj.Items) > 0 {
   254  		for _, o := range npObj.Elem().Items {
   255  			ot, ok := o.Val.(*ast.ObjectType)
   256  			if !ok {
   257  				break
   258  			}
   259  			var npConfig *api.NamespaceNodePoolConfiguration
   260  			if err := hcl.DecodeObject(&npConfig, ot.List); err != nil {
   261  				return err
   262  			}
   263  			result.NodePoolConfiguration = npConfig
   264  			break
   265  		}
   266  	}
   267  
   268  	if metaO := list.Filter("meta"); len(metaO.Items) > 0 {
   269  		for _, o := range metaO.Elem().Items {
   270  			var m map[string]interface{}
   271  			if err := hcl.DecodeObject(&m, o.Val); err != nil {
   272  				return err
   273  			}
   274  			if err := mapstructure.WeakDecode(m, &result.Meta); err != nil {
   275  				return err
   276  			}
   277  		}
   278  	}
   279  
   280  	return nil
   281  }