open-nomad/command/namespace_apply.go
Luiz Aoqui d5aa72190f
node pools: namespace integration (#17562)
Add structs and fields to support the Nomad Pools Governance Enterprise
feature of controlling node pool access via namespaces.

Nomad Enterprise allows users to specify a default node pool to be used
by jobs that don't specify one. In order to accomplish this, it's
necessary to distinguish between a job that explicitly uses the
`default` node pool and one that did not specify any.

If the `default` node pool is set during job canonicalization it's
impossible to do this, so this commit allows a job to have an empty node
pool value during registration but sets to `default` at the admission
controller mutator.

In order to guarantee state consistency the state store validates that
the job node pool is set and exists before inserting it.
2023-06-16 16:30:22 -04:00

282 lines
6.6 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package command
import (
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"strings"
"github.com/hashicorp/hcl"
"github.com/hashicorp/hcl/hcl/ast"
"github.com/hashicorp/nomad/api"
flaghelper "github.com/hashicorp/nomad/helper/flags"
"github.com/mitchellh/mapstructure"
"github.com/posener/complete"
)
type NamespaceApplyCommand struct {
Meta
}
func (c *NamespaceApplyCommand) Help() string {
helpText := `
Usage: nomad namespace apply [options] <input>
Apply is used to create or update a namespace. The specification file
will be read from stdin by specifying "-", otherwise a path to the file is
expected.
Instead of a file, you may instead pass the namespace name to create
or update as the only argument.
If ACLs are enabled, this command requires a management ACL token. In
federated clusters, the namespace will be created in the authoritative region
and will be replicated to all federated regions.
General Options:
` + generalOptionsUsage(usageOptsDefault|usageOptsNoNamespace) + `
Apply Options:
-quota
The quota to attach to the namespace.
-description
An optional description for the namespace.
-json
Parse the input as a JSON namespace specification.
`
return strings.TrimSpace(helpText)
}
func (c *NamespaceApplyCommand) AutocompleteFlags() complete.Flags {
return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
complete.Flags{
"-description": complete.PredictAnything,
"-quota": QuotaPredictor(c.Meta.Client),
"-json": complete.PredictNothing,
})
}
func (c *NamespaceApplyCommand) AutocompleteArgs() complete.Predictor {
return complete.PredictOr(
NamespacePredictor(c.Meta.Client, nil),
complete.PredictFiles("*.hcl"),
complete.PredictFiles("*.json"),
)
}
func (c *NamespaceApplyCommand) Synopsis() string {
return "Create or update a namespace"
}
func (c *NamespaceApplyCommand) Name() string { return "namespace apply" }
func (c *NamespaceApplyCommand) Run(args []string) int {
var jsonInput bool
var description, quota *string
flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
flags.Usage = func() { c.Ui.Output(c.Help()) }
flags.Var((flaghelper.FuncVar)(func(s string) error {
description = &s
return nil
}), "description", "")
flags.Var((flaghelper.FuncVar)(func(s string) error {
quota = &s
return nil
}), "quota", "")
flags.BoolVar(&jsonInput, "json", false, "")
if err := flags.Parse(args); err != nil {
return 1
}
// Check that we get exactly one argument
args = flags.Args()
if l := len(args); l != 1 {
c.Ui.Error("This command takes one argument: <input>")
c.Ui.Error(commandErrorText(c))
return 1
}
file := args[0]
var rawNamespace []byte
var err error
var namespace *api.Namespace
// Get the HTTP client
client, err := c.Meta.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
return 1
}
if fi, err := os.Stat(file); (file == "-" || err == nil) && !fi.IsDir() {
if quota != nil || description != nil {
c.Ui.Warn("Flags are ignored when a file is specified!")
}
if file == "-" {
rawNamespace, err = io.ReadAll(os.Stdin)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to read stdin: %v", err))
return 1
}
} else {
rawNamespace, err = os.ReadFile(file)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to read file: %v", err))
return 1
}
}
if jsonInput {
var jsonSpec api.Namespace
dec := json.NewDecoder(bytes.NewBuffer(rawNamespace))
if err := dec.Decode(&jsonSpec); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to parse quota: %v", err))
return 1
}
namespace = &jsonSpec
} else {
hclSpec, err := parseNamespaceSpec(rawNamespace)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error parsing quota specification: %s", err))
return 1
}
namespace = hclSpec
}
} else {
name := args[0]
// Validate we have at-least a name
if name == "" {
c.Ui.Error("Namespace name required")
return 1
}
// Lookup the given namespace
namespace, _, err = client.Namespaces().Info(name, nil)
if err != nil && !strings.Contains(err.Error(), "404") {
c.Ui.Error(fmt.Sprintf("Error looking up namespace: %s", err))
return 1
}
if namespace == nil {
namespace = &api.Namespace{
Name: name,
}
}
// Add what is set
if description != nil {
namespace.Description = *description
}
if quota != nil {
namespace.Quota = *quota
}
}
_, err = client.Namespaces().Register(namespace, nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error applying namespace: %s", err))
return 1
}
c.Ui.Output(fmt.Sprintf("Successfully applied namespace %q!", namespace.Name))
return 0
}
// parseNamespaceSpec is used to parse the namespace specification from HCL
func parseNamespaceSpec(input []byte) (*api.Namespace, error) {
root, err := hcl.ParseBytes(input)
if err != nil {
return nil, err
}
// Top-level item should be a list
list, ok := root.Node.(*ast.ObjectList)
if !ok {
return nil, fmt.Errorf("error parsing: root should be an object")
}
var spec api.Namespace
if err := parseNamespaceSpecImpl(&spec, list); err != nil {
return nil, err
}
return &spec, nil
}
// parseNamespaceSpec parses the quota namespace taking as input the AST tree
func parseNamespaceSpecImpl(result *api.Namespace, list *ast.ObjectList) error {
// Decode the full thing into a map[string]interface for ease
var m map[string]interface{}
if err := hcl.DecodeObject(&m, list); err != nil {
return err
}
delete(m, "capabilities")
delete(m, "meta")
delete(m, "node_pool_config")
// Decode the rest
if err := mapstructure.WeakDecode(m, result); err != nil {
return err
}
cObj := list.Filter("capabilities")
if len(cObj.Items) > 0 {
for _, o := range cObj.Elem().Items {
ot, ok := o.Val.(*ast.ObjectType)
if !ok {
break
}
var opts *api.NamespaceCapabilities
if err := hcl.DecodeObject(&opts, ot.List); err != nil {
return err
}
result.Capabilities = opts
break
}
}
npObj := list.Filter("node_pool_config")
if len(npObj.Items) > 0 {
for _, o := range npObj.Elem().Items {
ot, ok := o.Val.(*ast.ObjectType)
if !ok {
break
}
var npConfig *api.NamespaceNodePoolConfiguration
if err := hcl.DecodeObject(&npConfig, ot.List); err != nil {
return err
}
result.NodePoolConfiguration = npConfig
break
}
}
if metaO := list.Filter("meta"); len(metaO.Items) > 0 {
for _, o := range metaO.Elem().Items {
var m map[string]interface{}
if err := hcl.DecodeObject(&m, o.Val); err != nil {
return err
}
if err := mapstructure.WeakDecode(m, &result.Meta); err != nil {
return err
}
}
}
return nil
}