Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Intial implementation for #11807 #11813

Merged
merged 7 commits into from
Feb 24, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions api/namespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,17 @@ func (n *Namespaces) Delete(namespace string, q *WriteOptions) (*WriteMeta, erro

// Namespace is used to serialize a namespace.
type Namespace struct {
Name string
Description string
Quota string
CreateIndex uint64
ModifyIndex uint64
Name string
Description string
Quota string
Capabilities *NamespaceCapabilities `hcl:"capabilities,block"`
CreateIndex uint64
ModifyIndex uint64
}

type NamespaceCapabilities struct {
EnabledTaskDrivers []string `hcl:"enabled_task_drivers"`
DisabledTaskDrivers []string `hcl:"disabled_task_drivers"`
}

// NamespaceIndexSort is a wrapper to sort Namespaces by CreateIndex. We
Expand Down
174 changes: 144 additions & 30 deletions command/namespace_apply.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
package command

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"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"
)

Expand All @@ -15,10 +22,14 @@ type NamespaceApplyCommand struct {

func (c *NamespaceApplyCommand) Help() string {
helpText := `
Usage: nomad namespace apply [options] <namespace>
Usage: nomad namespace apply [options] <input>

Apply is used to create or update a namespace. It takes the namespace name to
create or update as its only argument.
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.

Expand All @@ -33,6 +44,9 @@ Apply Options:

-description
An optional description for the namespace.

-json
Parse the input as a JSON namespace specification.
`
return strings.TrimSpace(helpText)
}
Expand All @@ -42,6 +56,7 @@ func (c *NamespaceApplyCommand) AutocompleteFlags() complete.Flags {
complete.Flags{
"-description": complete.PredictAnything,
"-quota": QuotaPredictor(c.Meta.Client),
"-json": complete.PredictNothing,
})
}

Expand All @@ -56,6 +71,7 @@ func (c *NamespaceApplyCommand) Synopsis() string {
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)
Expand All @@ -68,6 +84,7 @@ func (c *NamespaceApplyCommand) Run(args []string) int {
quota = &s
return nil
}), "quota", "")
flags.BoolVar(&jsonInput, "json", false, "")

if err := flags.Parse(args); err != nil {
return 1
Expand All @@ -76,18 +93,15 @@ func (c *NamespaceApplyCommand) Run(args []string) int {
// Check that we get exactly one argument
args = flags.Args()
if l := len(args); l != 1 {
c.Ui.Error("This command takes one argument: <namespace>")
c.Ui.Error("This command takes one argument: <input>")
c.Ui.Error(commandErrorText(c))
return 1
}

name := args[0]

// Validate we have at-least a name
if name == "" {
c.Ui.Error("Namespace name required")
return 1
}
file := args[0]
var rawNamespace []byte
var err error
var namespace *api.Namespace

// Get the HTTP client
client, err := c.Meta.Client()
Expand All @@ -96,33 +110,133 @@ func (c *NamespaceApplyCommand) Run(args []string) int {
return 1
}

// Lookup the given namespace
ns, _, 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 _, err = os.Stat(file); file == "-" || err == nil {
if quota != nil || description != nil {
c.Ui.Warn("Flags are ignored when a file is specified!")
}

if ns == nil {
ns = &api.Namespace{
Name: name,
if file == "-" {
rawNamespace, err = ioutil.ReadAll(os.Stdin)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to read stdin: %v", err))
return 1
}
} else {
rawNamespace, err = ioutil.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]
apollo13 marked this conversation as resolved.
Show resolved Hide resolved

// Add what is set
if description != nil {
ns.Description = *description
}
if quota != nil {
ns.Quota = *quota
}
// 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,
}
}

_, err = client.Namespaces().Register(ns, nil)
// 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!", name))
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")

// 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
}
}

return nil
}
12 changes: 12 additions & 0 deletions command/namespace_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,22 @@ func (c *NamespaceStatusCommand) Run(args []string) int {

// formatNamespaceBasics formats the basic information of the namespace
func formatNamespaceBasics(ns *api.Namespace) string {
enabled_drivers := "*"
disabled_drivers := ""
if ns.Capabilities != nil {
if len(ns.Capabilities.EnabledTaskDrivers) != 0 {
enabled_drivers = strings.Join(ns.Capabilities.EnabledTaskDrivers, ",")
}
if len(ns.Capabilities.DisabledTaskDrivers) != 0 {
disabled_drivers = strings.Join(ns.Capabilities.DisabledTaskDrivers, ",")
}
}
basic := []string{
fmt.Sprintf("Name|%s", ns.Name),
fmt.Sprintf("Description|%s", ns.Description),
fmt.Sprintf("Quota|%s", ns.Quota),
fmt.Sprintf("EnabledDrivers|%s", enabled_drivers),
fmt.Sprintf("DisabledDrivers|%s", disabled_drivers),
}

return formatKV(basic)
Expand Down
1 change: 1 addition & 0 deletions nomad/job_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ func NewJobEndpoints(s *Server) *Job {
validators: []jobValidator{
jobConnectHook{},
jobExposeCheckHook{},
jobNamespaceConstraintCheckHook{srv: s},
jobValidate{},
&memoryOversubscriptionValidate{srv: s},
},
Expand Down
56 changes: 56 additions & 0 deletions nomad/job_endpoint_validators.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package nomad

import (
"github.com/hashicorp/nomad/nomad/structs"
"github.com/pkg/errors"
)

type jobNamespaceConstraintCheckHook struct {
srv *Server
}

func (jobNamespaceConstraintCheckHook) Name() string {
return "namespace-constraint-check"
}

func (c jobNamespaceConstraintCheckHook) Validate(job *structs.Job) (warnings []error, err error) {
// This was validated before and matches the WriteRequest namespace
ns, err := c.srv.State().NamespaceByName(nil, job.Namespace)
if err != nil {
return nil, err
}
if ns == nil {
return nil, errors.Errorf("requested namespace %s does not exist", job.Namespace)
apollo13 marked this conversation as resolved.
Show resolved Hide resolved
}

for _, tg := range job.TaskGroups {
for _, t := range tg.Tasks {
if !taskValidateDriver(t, ns) {
return nil, errors.Errorf(
"used task driver '%s' in %s[%s] is not allowed in namespace %s",
t.Driver, tg.Name, t.Name, ns.Name)
}
}
apollo13 marked this conversation as resolved.
Show resolved Hide resolved
}
return nil, nil
}

func taskValidateDriver(task *structs.Task, ns *structs.Namespace) bool {
if ns.Capabilities == nil {
return true
}
allow := len(ns.Capabilities.EnabledTaskDrivers) == 0
for _, d := range ns.Capabilities.EnabledTaskDrivers {
if task.Driver == d {
allow = true
break
}
}
for _, d := range ns.Capabilities.DisabledTaskDrivers {
if task.Driver == d {
allow = false
break
}
}
return allow
}
15 changes: 15 additions & 0 deletions nomad/job_endpoint_validators_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package nomad

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestJobNamespaceConstraintCheckHook_Name(t *testing.T) {
t.Parallel()

require.Equal(t, "namespace-constraint-check", new(jobNamespaceConstraintCheckHook).Name())
}

// TODO: More tests
tgross marked this conversation as resolved.
Show resolved Hide resolved
Loading