Skip to content

Commit

Permalink
namespaces: allow enabling/disabling allowed drivers per namespace
Browse files Browse the repository at this point in the history
  • Loading branch information
apollo13 authored Feb 24, 2022
1 parent 5b65c97 commit b84f70a
Show file tree
Hide file tree
Showing 10 changed files with 405 additions and 41 deletions.
3 changes: 3 additions & 0 deletions .changelog/11807.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
namespaces: Allow enabling/disabling allowed drivers per namespace.
```
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]

// 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
65 changes: 65 additions & 0 deletions nomad/job_endpoint_validators.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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("job %q is in nonexistent namespace %q", job.ID, job.Namespace)
}

var disallowedDrivers []string
for _, tg := range job.TaskGroups {
for _, t := range tg.Tasks {
if !taskValidateDriver(t, ns) {
disallowedDrivers = append(disallowedDrivers, t.Driver)
}
}
}
if len(disallowedDrivers) > 0 {
if len(disallowedDrivers) == 1 {
return nil, errors.Errorf(
"used task driver %q is not allowed in namespace %q", disallowedDrivers[0], ns.Name)

} else {
return nil, errors.Errorf(
"used task drivers %q are not allowed in namespace %q", disallowedDrivers, ns.Name)
}
}
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
}
Loading

0 comments on commit b84f70a

Please sign in to comment.