From 19810365f603c552819a433c0e905f2673f3feaa Mon Sep 17 00:00:00 2001
From: Drew Bailey <2614075+drewbailey@users.noreply.github.com>
Date: Fri, 17 Jul 2020 10:41:45 -0400
Subject: [PATCH] oss compoments for multi-vault namespaces
adds in oss components to support enterprise multi-vault namespace feature
upgrade specific doc on vault multi-namespaces
vault docs
update test to reflect new error
---
api/jobs.go | 4 +
api/jobs_test.go | 8 ++
api/tasks.go | 4 +
.../taskrunner/task_runner_getters.go | 8 +-
.../taskrunner/template/template.go | 9 +++
.../taskrunner/template/template_test.go | 35 ++++++++
.../allocrunner/taskrunner/template_hook.go | 10 +++
client/client.go | 1 +
command/agent/job_endpoint.go | 40 ++++++----
command/agent/job_endpoint_test.go | 24 +++---
command/job_run.go | 18 ++++-
helper/funcs.go | 21 +++++
helper/funcs_test.go | 46 +++++++++++
jobspec/parse.go | 1 +
jobspec/parse_test.go | 1 +
jobspec/test-fixtures/basic.hcl | 3 +-
nomad/job_endpoint.go | 6 ++
nomad/job_endpoint_oss.go | 22 +++++-
nomad/job_endpoint_test.go | 51 ++++++++++++
nomad/server.go | 3 +-
nomad/server_setup_oss.go | 4 +
nomad/structs/diff_test.go | 16 ++++
nomad/structs/funcs.go | 20 +++++
nomad/structs/structs.go | 6 ++
nomad/vault.go | 79 +++++++++++++++----
nomad/vault_test.go | 72 ++++++++---------
vendor/github.com/hashicorp/nomad/api/jobs.go | 4 +
.../github.com/hashicorp/nomad/api/tasks.go | 4 +
.../pages/docs/job-specification/vault.mdx | 20 +++++
.../pages/docs/upgrade/upgrade-specific.mdx | 10 +++
30 files changed, 467 insertions(+), 83 deletions(-)
diff --git a/api/jobs.go b/api/jobs.go
index 8308f0a7fc6..bd79537252f 100644
--- a/api/jobs.go
+++ b/api/jobs.go
@@ -787,6 +787,7 @@ type Job struct {
Meta map[string]string
ConsulToken *string `mapstructure:"consul_token"`
VaultToken *string `mapstructure:"vault_token"`
+ VaultNamespace *string `mapstructure:"vault_namespace"`
NomadTokenID *string `mapstructure:"nomad_token_id"`
Status *string
StatusDescription *string
@@ -850,6 +851,9 @@ func (j *Job) Canonicalize() {
if j.VaultToken == nil {
j.VaultToken = stringToPtr("")
}
+ if j.VaultNamespace == nil {
+ j.VaultNamespace = stringToPtr("")
+ }
if j.NomadTokenID == nil {
j.NomadTokenID = stringToPtr("")
}
diff --git a/api/jobs_test.go b/api/jobs_test.go
index 150ecfc0831..4eb62e4e519 100644
--- a/api/jobs_test.go
+++ b/api/jobs_test.go
@@ -243,6 +243,7 @@ func TestJobs_Canonicalize(t *testing.T) {
AllAtOnce: boolToPtr(false),
ConsulToken: stringToPtr(""),
VaultToken: stringToPtr(""),
+ VaultNamespace: stringToPtr(""),
NomadTokenID: stringToPtr(""),
Status: stringToPtr(""),
StatusDescription: stringToPtr(""),
@@ -333,6 +334,7 @@ func TestJobs_Canonicalize(t *testing.T) {
AllAtOnce: boolToPtr(false),
ConsulToken: stringToPtr(""),
VaultToken: stringToPtr(""),
+ VaultNamespace: stringToPtr(""),
NomadTokenID: stringToPtr(""),
Status: stringToPtr(""),
StatusDescription: stringToPtr(""),
@@ -406,6 +408,7 @@ func TestJobs_Canonicalize(t *testing.T) {
AllAtOnce: boolToPtr(false),
ConsulToken: stringToPtr(""),
VaultToken: stringToPtr(""),
+ VaultNamespace: stringToPtr(""),
NomadTokenID: stringToPtr(""),
Stop: boolToPtr(false),
Stable: boolToPtr(false),
@@ -572,6 +575,7 @@ func TestJobs_Canonicalize(t *testing.T) {
AllAtOnce: boolToPtr(false),
ConsulToken: stringToPtr(""),
VaultToken: stringToPtr(""),
+ VaultNamespace: stringToPtr(""),
NomadTokenID: stringToPtr(""),
Stop: boolToPtr(false),
Stable: boolToPtr(false),
@@ -730,6 +734,7 @@ func TestJobs_Canonicalize(t *testing.T) {
AllAtOnce: boolToPtr(false),
ConsulToken: stringToPtr(""),
VaultToken: stringToPtr(""),
+ VaultNamespace: stringToPtr(""),
NomadTokenID: stringToPtr(""),
Stop: boolToPtr(false),
Stable: boolToPtr(false),
@@ -816,6 +821,7 @@ func TestJobs_Canonicalize(t *testing.T) {
AllAtOnce: boolToPtr(false),
ConsulToken: stringToPtr(""),
VaultToken: stringToPtr(""),
+ VaultNamespace: stringToPtr(""),
NomadTokenID: stringToPtr(""),
Stop: boolToPtr(false),
Stable: boolToPtr(false),
@@ -981,6 +987,7 @@ func TestJobs_Canonicalize(t *testing.T) {
AllAtOnce: boolToPtr(false),
ConsulToken: stringToPtr(""),
VaultToken: stringToPtr(""),
+ VaultNamespace: stringToPtr(""),
NomadTokenID: stringToPtr(""),
Stop: boolToPtr(false),
Stable: boolToPtr(false),
@@ -1144,6 +1151,7 @@ func TestJobs_Canonicalize(t *testing.T) {
AllAtOnce: boolToPtr(false),
ConsulToken: stringToPtr(""),
VaultToken: stringToPtr(""),
+ VaultNamespace: stringToPtr(""),
NomadTokenID: stringToPtr(""),
Stop: boolToPtr(false),
Stable: boolToPtr(false),
diff --git a/api/tasks.go b/api/tasks.go
index b9b79af5470..8275813e6ee 100644
--- a/api/tasks.go
+++ b/api/tasks.go
@@ -812,6 +812,7 @@ func (tmpl *Template) Canonicalize() {
type Vault struct {
Policies []string
+ Namespace *string `mapstructure:"namespace"`
Env *bool
ChangeMode *string `mapstructure:"change_mode"`
ChangeSignal *string `mapstructure:"change_signal"`
@@ -821,6 +822,9 @@ func (v *Vault) Canonicalize() {
if v.Env == nil {
v.Env = boolToPtr(true)
}
+ if v.Namespace == nil {
+ v.Namespace = stringToPtr("")
+ }
if v.ChangeMode == nil {
v.ChangeMode = stringToPtr("restart")
}
diff --git a/client/allocrunner/taskrunner/task_runner_getters.go b/client/allocrunner/taskrunner/task_runner_getters.go
index d962fcab5b6..5801297c386 100644
--- a/client/allocrunner/taskrunner/task_runner_getters.go
+++ b/client/allocrunner/taskrunner/task_runner_getters.go
@@ -57,7 +57,13 @@ func (tr *TaskRunner) setVaultToken(token string) {
tr.vaultToken = token
// Update the task's environment
- tr.envBuilder.SetVaultToken(token, tr.clientConfig.VaultConfig.Namespace, tr.task.Vault.Env)
+ taskNamespace := tr.task.Vault.Namespace
+
+ ns := tr.clientConfig.VaultConfig.Namespace
+ if taskNamespace != "" {
+ ns = taskNamespace
+ }
+ tr.envBuilder.SetVaultToken(token, ns, tr.task.Vault.Env)
}
// getDriverHandle returns a driver handle.
diff --git a/client/allocrunner/taskrunner/template/template.go b/client/allocrunner/taskrunner/template/template.go
index a0e7c2bbd6b..caf3f99bcbf 100644
--- a/client/allocrunner/taskrunner/template/template.go
+++ b/client/allocrunner/taskrunner/template/template.go
@@ -87,6 +87,9 @@ type TaskTemplateManagerConfig struct {
// VaultToken is the Vault token for the task.
VaultToken string
+ // VaultNamespace is the Vault namespace for the task
+ VaultNamespace string
+
// TaskDir is the task's directory
TaskDir string
@@ -655,9 +658,15 @@ func newRunnerConfig(config *TaskTemplateManagerConfig,
if cc.VaultConfig != nil && cc.VaultConfig.IsEnabled() {
conf.Vault.Address = &cc.VaultConfig.Addr
conf.Vault.Token = &config.VaultToken
+
+ // Set the Vault Namespace. Passed in Task config has
+ // highest precedence.
if config.ClientConfig.VaultConfig.Namespace != "" {
conf.Vault.Namespace = &config.ClientConfig.VaultConfig.Namespace
}
+ if config.VaultNamespace != "" {
+ conf.Vault.Namespace = &config.VaultNamespace
+ }
if strings.HasPrefix(cc.VaultConfig.Addr, "https") || cc.VaultConfig.TLSCertFile != "" {
skipVerify := cc.VaultConfig.TLSSkipVerify != nil && *cc.VaultConfig.TLSSkipVerify
diff --git a/client/allocrunner/taskrunner/template/template_test.go b/client/allocrunner/taskrunner/template/template_test.go
index 3b51af3f4b3..459149ca4fd 100644
--- a/client/allocrunner/taskrunner/template/template_test.go
+++ b/client/allocrunner/taskrunner/template/template_test.go
@@ -1377,6 +1377,41 @@ func TestTaskTemplateManager_Config_VaultNamespace(t *testing.T) {
assert.Equal(testNS, *ctconf.Vault.Namespace, "Vault Namespace Value")
}
+// TestTaskTemplateManager_Config_VaultNamespace asserts the Vault namespace setting is
+// propagated to consul-template's configuration.
+func TestTaskTemplateManager_Config_VaultNamespace_TaskOverride(t *testing.T) {
+ t.Parallel()
+ assert := assert.New(t)
+
+ testNS := "test-namespace"
+ c := config.DefaultConfig()
+ c.Node = mock.Node()
+ c.VaultConfig = &sconfig.VaultConfig{
+ Enabled: helper.BoolToPtr(true),
+ Addr: "https://localhost/",
+ TLSServerName: "notlocalhost",
+ Namespace: testNS,
+ }
+
+ alloc := mock.Alloc()
+ overriddenNS := "new-namespace"
+
+ // Set the template manager config vault namespace
+ config := &TaskTemplateManagerConfig{
+ ClientConfig: c,
+ VaultToken: "token",
+ VaultNamespace: overriddenNS,
+ EnvBuilder: taskenv.NewBuilder(c.Node, alloc, alloc.Job.TaskGroups[0].Tasks[0], c.Region),
+ }
+
+ ctmplMapping, err := parseTemplateConfigs(config)
+ assert.Nil(err, "Parsing Templates")
+
+ ctconf, err := newRunnerConfig(config, ctmplMapping)
+ assert.Nil(err, "Building Runner Config")
+ assert.Equal(overriddenNS, *ctconf.Vault.Namespace, "Vault Namespace Value")
+}
+
func TestTaskTemplateManager_BlockedEvents(t *testing.T) {
// The tests sets a template that need keys 0, 1, 2, 3, 4,
// then subsequently sets 0, 1, 2 keys
diff --git a/client/allocrunner/taskrunner/template_hook.go b/client/allocrunner/taskrunner/template_hook.go
index 1c76d144792..58150bbabb7 100644
--- a/client/allocrunner/taskrunner/template_hook.go
+++ b/client/allocrunner/taskrunner/template_hook.go
@@ -47,6 +47,9 @@ type templateHook struct {
// vaultToken is the current Vault token
vaultToken string
+ // vaultNamespace is the current Vault namespace
+ vaultNamespace string
+
// taskDir is the task directory
taskDir string
}
@@ -75,6 +78,12 @@ func (h *templateHook) Prestart(ctx context.Context, req *interfaces.TaskPrestar
// Store the current Vault token and the task directory
h.taskDir = req.TaskDir.Dir
h.vaultToken = req.VaultToken
+
+ // Set vault namespace if specified
+ if req.Task.Vault != nil {
+ h.vaultNamespace = req.Task.Vault.Namespace
+ }
+
unblockCh, err := h.newManager()
if err != nil {
return err
@@ -98,6 +107,7 @@ func (h *templateHook) newManager() (unblock chan struct{}, err error) {
Templates: h.config.templates,
ClientConfig: h.config.clientConfig,
VaultToken: h.vaultToken,
+ VaultNamespace: h.vaultNamespace,
TaskDir: h.taskDir,
EnvBuilder: h.config.envBuilder,
MaxTemplateEventRate: template.DefaultMaxTemplateEventRate,
diff --git a/client/client.go b/client/client.go
index 3cac9ddcde5..31a62c8ad68 100644
--- a/client/client.go
+++ b/client/client.go
@@ -2457,6 +2457,7 @@ func (c *Client) deriveToken(alloc *structs.Allocation, taskNames []string, vcli
}
// Derive the tokens
+ // namespace is handled via nomad/vault
var resp structs.DeriveVaultTokenResponse
if err := c.RPC("Node.DeriveVaultToken", &req, &resp); err != nil {
vlogger.Error("error making derive token RPC", "error", err)
diff --git a/command/agent/job_endpoint.go b/command/agent/job_endpoint.go
index f112372f609..a081f44529c 100644
--- a/command/agent/job_endpoint.go
+++ b/command/agent/job_endpoint.go
@@ -773,22 +773,23 @@ func ApiJobToStructJob(job *api.Job) *structs.Job {
job.Canonicalize()
j := &structs.Job{
- Stop: *job.Stop,
- Region: *job.Region,
- Namespace: *job.Namespace,
- ID: *job.ID,
- ParentID: *job.ParentID,
- Name: *job.Name,
- Type: *job.Type,
- Priority: *job.Priority,
- AllAtOnce: *job.AllAtOnce,
- Datacenters: job.Datacenters,
- Payload: job.Payload,
- Meta: job.Meta,
- ConsulToken: *job.ConsulToken,
- VaultToken: *job.VaultToken,
- Constraints: ApiConstraintsToStructs(job.Constraints),
- Affinities: ApiAffinitiesToStructs(job.Affinities),
+ Stop: *job.Stop,
+ Region: *job.Region,
+ Namespace: *job.Namespace,
+ ID: *job.ID,
+ ParentID: *job.ParentID,
+ Name: *job.Name,
+ Type: *job.Type,
+ Priority: *job.Priority,
+ AllAtOnce: *job.AllAtOnce,
+ Datacenters: job.Datacenters,
+ Payload: job.Payload,
+ Meta: job.Meta,
+ ConsulToken: *job.ConsulToken,
+ VaultToken: *job.VaultToken,
+ VaultNamespace: *job.VaultNamespace,
+ Constraints: ApiConstraintsToStructs(job.Constraints),
+ Affinities: ApiAffinitiesToStructs(job.Affinities),
}
// Update has been pushed into the task groups. stagger and max_parallel are
@@ -976,6 +977,12 @@ func ApiTgToStructsTG(job *structs.Job, taskGroup *api.TaskGroup, tg *structs.Ta
for l, task := range taskGroup.Tasks {
t := &structs.Task{}
ApiTaskToStructsTask(task, t)
+
+ // Set the tasks vault namespace from Job if it was not
+ // specified by the task or group
+ if t.Vault != nil && t.Vault.Namespace == "" && job.VaultNamespace != "" {
+ t.Vault.Namespace = job.VaultNamespace
+ }
tg.Tasks[l] = t
}
}
@@ -1089,6 +1096,7 @@ func ApiTaskToStructsTask(apiTask *api.Task, structsTask *structs.Task) {
if apiTask.Vault != nil {
structsTask.Vault = &structs.Vault{
Policies: apiTask.Vault.Policies,
+ Namespace: *apiTask.Vault.Namespace,
Env: *apiTask.Vault.Env,
ChangeMode: *apiTask.Vault.ChangeMode,
ChangeSignal: *apiTask.Vault.ChangeSignal,
diff --git a/command/agent/job_endpoint_test.go b/command/agent/job_endpoint_test.go
index cb3fbfb54d0..8549ccbc30c 100644
--- a/command/agent/job_endpoint_test.go
+++ b/command/agent/job_endpoint_test.go
@@ -2121,6 +2121,7 @@ func TestJobs_ApiJobToStructsJob(t *testing.T) {
},
},
Vault: &api.Vault{
+ Namespace: helper.StringToPtr("ns1"),
Policies: []string{"a", "b", "c"},
Env: helper.BoolToPtr(true),
ChangeMode: helper.StringToPtr("c"),
@@ -2149,6 +2150,7 @@ func TestJobs_ApiJobToStructsJob(t *testing.T) {
},
ConsulToken: helper.StringToPtr("abc123"),
VaultToken: helper.StringToPtr("def456"),
+ VaultNamespace: helper.StringToPtr("ghi789"),
Status: helper.StringToPtr("status"),
StatusDescription: helper.StringToPtr("status_desc"),
Version: helper.Uint64ToPtr(10),
@@ -2158,16 +2160,17 @@ func TestJobs_ApiJobToStructsJob(t *testing.T) {
}
expected := &structs.Job{
- Stop: true,
- Region: "global",
- Namespace: "foo",
- ID: "foo",
- ParentID: "lol",
- Name: "name",
- Type: "service",
- Priority: 50,
- AllAtOnce: true,
- Datacenters: []string{"dc1", "dc2"},
+ Stop: true,
+ Region: "global",
+ Namespace: "foo",
+ VaultNamespace: "ghi789",
+ ID: "foo",
+ ParentID: "lol",
+ Name: "name",
+ Type: "service",
+ Priority: 50,
+ AllAtOnce: true,
+ Datacenters: []string{"dc1", "dc2"},
Constraints: []*structs.Constraint{
{
LTarget: "a",
@@ -2488,6 +2491,7 @@ func TestJobs_ApiJobToStructsJob(t *testing.T) {
},
},
Vault: &structs.Vault{
+ Namespace: "ns1",
Policies: []string{"a", "b", "c"},
Env: true,
ChangeMode: "c",
diff --git a/command/job_run.go b/command/job_run.go
index 3af389b2183..3ad750c5370 100644
--- a/command/job_run.go
+++ b/command/job_run.go
@@ -101,6 +101,11 @@ Run Options:
the job file. This overrides the token found in $VAULT_TOKEN environment
variable and that found in the job.
+ -vault-namespace
+ If set, the passed Vault namespace is stored in the job before sending to the
+ Nomad servers. This overrides the namespace found in $VAULT_NAMESPACE environment
+ variable and that found in the job.
+
-verbose
Display full information.
`
@@ -119,6 +124,7 @@ func (c *JobRunCommand) AutocompleteFlags() complete.Flags {
"-verbose": complete.PredictNothing,
"-consul-token": complete.PredictNothing,
"-vault-token": complete.PredictAnything,
+ "-vault-namespace": complete.PredictAnything,
"-output": complete.PredictNothing,
"-policy-override": complete.PredictNothing,
"-preserve-counts": complete.PredictNothing,
@@ -133,7 +139,7 @@ func (c *JobRunCommand) Name() string { return "job run" }
func (c *JobRunCommand) Run(args []string) int {
var detach, verbose, output, override, preserveCounts bool
- var checkIndexStr, consulToken, vaultToken string
+ var checkIndexStr, consulToken, vaultToken, vaultNamespace string
flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
flags.Usage = func() { c.Ui.Output(c.Help()) }
@@ -145,6 +151,7 @@ func (c *JobRunCommand) Run(args []string) int {
flags.StringVar(&checkIndexStr, "check-index", "", "")
flags.StringVar(&consulToken, "consul-token", "", "")
flags.StringVar(&vaultToken, "vault-token", "", "")
+ flags.StringVar(&vaultNamespace, "vault-namespace", "", "")
if err := flags.Parse(args); err != nil {
return 1
@@ -213,6 +220,15 @@ func (c *JobRunCommand) Run(args []string) int {
job.VaultToken = helper.StringToPtr(vaultToken)
}
+ // Parse the Vault namespace
+ if vaultNamespace == "" {
+ vaultNamespace = os.Getenv("VAULT_NAMESPACE")
+ }
+
+ if vaultNamespace != "" {
+ job.VaultNamespace = helper.StringToPtr(vaultNamespace)
+ }
+
if output {
req := struct {
Job *api.Job
diff --git a/helper/funcs.go b/helper/funcs.go
index c75294a1b69..e6390720cbe 100644
--- a/helper/funcs.go
+++ b/helper/funcs.go
@@ -3,6 +3,7 @@ package helper
import (
"crypto/sha512"
"fmt"
+ "path/filepath"
"reflect"
"regexp"
"strings"
@@ -461,3 +462,23 @@ func RemoveEqualFold(xs *[]string, search string) {
}
}
}
+
+// CheckNamespaceScope ensures that the provided namespace is equal to
+// or a parent of the requested namespaces. Returns requested namespaces
+// which are not equal to or a child of the provided namespace.
+func CheckNamespaceScope(provided string, requested []string) []string {
+ var offending []string
+ for _, ns := range requested {
+ rel, err := filepath.Rel(provided, ns)
+ if err != nil {
+ offending = append(offending, ns)
+ // If relative path requires ".." it's not a child
+ } else if strings.Contains(rel, "..") {
+ offending = append(offending, ns)
+ }
+ }
+ if len(offending) > 0 {
+ return offending
+ }
+ return nil
+}
diff --git a/helper/funcs_test.go b/helper/funcs_test.go
index cff3e9c21cf..ea6f91bdd23 100644
--- a/helper/funcs_test.go
+++ b/helper/funcs_test.go
@@ -5,6 +5,8 @@ import (
"reflect"
"sort"
"testing"
+
+ "github.com/stretchr/testify/require"
)
func TestSliceStringIsSubset(t *testing.T) {
@@ -157,3 +159,47 @@ func BenchmarkCleanEnvVar(b *testing.B) {
CleanEnvVar(in, replacement)
}
}
+
+func TestCheckNamespaceScope(t *testing.T) {
+ cases := []struct {
+ desc string
+ provided string
+ requested []string
+ offending []string
+ }{
+ {
+ desc: "root ns requesting namespace",
+ provided: "",
+ requested: []string{"engineering"},
+ },
+ {
+ desc: "matching parent ns with child",
+ provided: "engineering",
+ requested: []string{"engineering", "engineering/sub-team"},
+ },
+ {
+ desc: "mismatch ns",
+ provided: "engineering",
+ requested: []string{"finance", "engineering/sub-team", "eng"},
+ offending: []string{"finance", "eng"},
+ },
+ {
+ desc: "mismatch child",
+ provided: "engineering/sub-team",
+ requested: []string{"engineering/new-team", "engineering/sub-team", "engineering/sub-team/child"},
+ offending: []string{"engineering/new-team"},
+ },
+ {
+ desc: "matching prefix",
+ provided: "engineering",
+ requested: []string{"engineering/new-team", "engineering/new-team/sub-team"},
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ offending := CheckNamespaceScope(tc.provided, tc.requested)
+ require.Equal(t, offending, tc.offending)
+ })
+ }
+}
diff --git a/jobspec/parse.go b/jobspec/parse.go
index 7d386abd8a6..f6e7b8584c8 100644
--- a/jobspec/parse.go
+++ b/jobspec/parse.go
@@ -506,6 +506,7 @@ func parseVault(result *api.Vault, list *ast.ObjectList) error {
// Check for invalid keys
valid := []string{
+ "namespace",
"policies",
"env",
"change_mode",
diff --git a/jobspec/parse_test.go b/jobspec/parse_test.go
index 73491645060..dfc64638694 100644
--- a/jobspec/parse_test.go
+++ b/jobspec/parse_test.go
@@ -332,6 +332,7 @@ func TestParse(t *testing.T) {
},
},
Vault: &api.Vault{
+ Namespace: helper.StringToPtr("ns1"),
Policies: []string{"foo", "bar"},
Env: helper.BoolToPtr(true),
ChangeMode: helper.StringToPtr(structs.VaultChangeModeRestart),
diff --git a/jobspec/test-fixtures/basic.hcl b/jobspec/test-fixtures/basic.hcl
index 8b2f9ef74d6..1b99ee725ea 100644
--- a/jobspec/test-fixtures/basic.hcl
+++ b/jobspec/test-fixtures/basic.hcl
@@ -292,7 +292,8 @@ job "binstore-storagelocker" {
}
vault {
- policies = ["foo", "bar"]
+ namespace = "ns1"
+ policies = ["foo", "bar"]
}
template {
diff --git a/nomad/job_endpoint.go b/nomad/job_endpoint.go
index 1980dbd01da..65b32d95d5b 100644
--- a/nomad/job_endpoint.go
+++ b/nomad/job_endpoint.go
@@ -232,6 +232,12 @@ func (j *Job) Register(args *structs.JobRegisterRequest, reply *structs.JobRegis
return err
}
+ // Check Namespaces
+ namespaceErr := j.multiVaultNamespaceValidation(policies, s)
+ if namespaceErr != nil {
+ return namespaceErr
+ }
+
// If we are given a root token it can access all policies
if !lib.StrContains(allowedPolicies, "root") {
flatPolicies := structs.VaultPoliciesSet(policies)
diff --git a/nomad/job_endpoint_oss.go b/nomad/job_endpoint_oss.go
index 08755c47a3d..b7ad70b1ba7 100644
--- a/nomad/job_endpoint_oss.go
+++ b/nomad/job_endpoint_oss.go
@@ -2,7 +2,13 @@
package nomad
-import "github.com/hashicorp/nomad/nomad/structs"
+import (
+ "fmt"
+ "strings"
+
+ "github.com/hashicorp/nomad/nomad/structs"
+ vapi "github.com/hashicorp/vault/api"
+)
// enforceSubmitJob is used to check any Sentinel policies for the submit-job scope
func (j *Job) enforceSubmitJob(override bool, job *structs.Job) (error, error) {
@@ -23,3 +29,17 @@ func (j *Job) multiregionStart(args *structs.JobRegisterRequest, reply *structs.
func (j *Job) interpolateMultiregionFields(args *structs.JobPlanRequest) error {
return nil
}
+
+// multiVaultNamespaceValidation provides a convience check to ensure
+// multiple vault namespaces were not requested, this returns an early friendly
+// error before job registry and further feature checks.
+func (j *Job) multiVaultNamespaceValidation(
+ policies map[string]map[string]*structs.Vault,
+ s *vapi.Secret,
+) error {
+ requestedNamespaces := structs.VaultNamespaceSet(policies)
+ if len(requestedNamespaces) > 0 {
+ return fmt.Errorf("multiple vault namespaces requires Nomad Enterprise, Namespaces: %s", strings.Join(requestedNamespaces, ", "))
+ }
+ return nil
+}
diff --git a/nomad/job_endpoint_test.go b/nomad/job_endpoint_test.go
index 9edd97c279c..4be53e0338b 100644
--- a/nomad/job_endpoint_test.go
+++ b/nomad/job_endpoint_test.go
@@ -1490,6 +1490,57 @@ func TestJobEndpoint_Register_Vault_Policies(t *testing.T) {
}
}
+func TestJobEndpoint_Register_Vault_MultiNamespaces(t *testing.T) {
+ t.Parallel()
+
+ s1, cleanupS1 := TestServer(t, func(c *Config) {
+ c.NumSchedulers = 0 // Prevent automatic dequeue
+ })
+ defer cleanupS1()
+ codec := rpcClient(t, s1)
+ testutil.WaitForLeader(t, s1.RPC)
+
+ // Enable vault
+ tr, f := true, false
+ s1.config.VaultConfig.Enabled = &tr
+ s1.config.VaultConfig.AllowUnauthenticated = &f
+
+ // Replace the Vault Client on the server
+ tvc := &TestVaultClient{}
+ s1.vault = tvc
+
+ goodToken := uuid.Generate()
+ goodPolicies := []string{"foo", "bar", "baz"}
+ tvc.SetLookupTokenAllowedPolicies(goodToken, goodPolicies)
+
+ // Create the register request with a job asking for a vault policy but
+ // don't send a Vault token
+ job := mock.Job()
+ job.VaultToken = goodToken
+ job.TaskGroups[0].Tasks[0].Vault = &structs.Vault{
+ Namespace: "ns1",
+ Policies: []string{"foo"},
+ ChangeMode: structs.VaultChangeModeRestart,
+ }
+ req := &structs.JobRegisterRequest{
+ Job: job,
+ WriteRequest: structs.WriteRequest{
+ Region: "global",
+ Namespace: job.Namespace,
+ },
+ }
+
+ // Fetch the response
+ var resp structs.JobRegisterResponse
+ err := msgpackrpc.CallWithCodec(codec, "Job.Register", req, &resp)
+ // OSS or Ent check
+ if s1.EnterpriseState.Features() == 0 {
+ require.Contains(t, err.Error(), "multiple vault namespaces requires Nomad Enterprise")
+ } else {
+ require.NoError(t, err)
+ }
+}
+
// TestJobEndpoint_Register_SemverConstraint asserts that semver ordering is
// used when evaluating semver constraints.
func TestJobEndpoint_Register_SemverConstraint(t *testing.T) {
diff --git a/nomad/server.go b/nomad/server.go
index fd378021df4..ed36d6074fc 100644
--- a/nomad/server.go
+++ b/nomad/server.go
@@ -1061,7 +1061,8 @@ func (s *Server) setupConsul(consulACLs consul.ACLsAPI) {
// setupVaultClient is used to set up the Vault API client.
func (s *Server) setupVaultClient() error {
- v, err := NewVaultClient(s.config.VaultConfig, s.logger, s.purgeVaultAccessors)
+ delegate := s.entVaultDelegate()
+ v, err := NewVaultClient(s.config.VaultConfig, s.logger, s.purgeVaultAccessors, delegate)
if err != nil {
return err
}
diff --git a/nomad/server_setup_oss.go b/nomad/server_setup_oss.go
index 267638d01b8..f5ca226cbda 100644
--- a/nomad/server_setup_oss.go
+++ b/nomad/server_setup_oss.go
@@ -24,3 +24,7 @@ func (s *Server) setupEnterprise(config *Config) error {
return nil
}
func (s *Server) startEnterpriseBackground() {}
+
+func (s *Server) entVaultDelegate() *VaultNoopDelegate {
+ return &VaultNoopDelegate{}
+}
diff --git a/nomad/structs/diff_test.go b/nomad/structs/diff_test.go
index 9fc774d6bb9..900f80c9b15 100644
--- a/nomad/structs/diff_test.go
+++ b/nomad/structs/diff_test.go
@@ -5855,6 +5855,7 @@ func TestTaskDiff(t *testing.T) {
Name: "Vault edited",
Old: &Task{
Vault: &Vault{
+ Namespace: "ns1",
Policies: []string{"foo", "bar"},
Env: true,
ChangeMode: "signal",
@@ -5863,6 +5864,7 @@ func TestTaskDiff(t *testing.T) {
},
New: &Task{
Vault: &Vault{
+ Namespace: "ns2",
Policies: []string{"bar", "baz"},
Env: false,
ChangeMode: "restart",
@@ -5894,6 +5896,12 @@ func TestTaskDiff(t *testing.T) {
Old: "true",
New: "false",
},
+ {
+ Type: DiffTypeEdited,
+ Name: "Namespace",
+ Old: "ns1",
+ New: "ns2",
+ },
},
Objects: []*ObjectDiff{
{
@@ -5924,6 +5932,7 @@ func TestTaskDiff(t *testing.T) {
Contextual: true,
Old: &Task{
Vault: &Vault{
+ Namespace: "ns1",
Policies: []string{"foo", "bar"},
Env: true,
ChangeMode: "signal",
@@ -5932,6 +5941,7 @@ func TestTaskDiff(t *testing.T) {
},
New: &Task{
Vault: &Vault{
+ Namespace: "ns1",
Policies: []string{"bar", "baz"},
Env: true,
ChangeMode: "signal",
@@ -5963,6 +5973,12 @@ func TestTaskDiff(t *testing.T) {
Old: "true",
New: "true",
},
+ {
+ Type: DiffTypeNone,
+ Name: "Namespace",
+ Old: "ns1",
+ New: "ns1",
+ },
},
Objects: []*ObjectDiff{
{
diff --git a/nomad/structs/funcs.go b/nomad/structs/funcs.go
index 855ab7ff0e1..f5d9bdd60d3 100644
--- a/nomad/structs/funcs.go
+++ b/nomad/structs/funcs.go
@@ -323,6 +323,26 @@ func VaultPoliciesSet(policies map[string]map[string]*Vault) []string {
return flattened
}
+// VaultNaVaultNamespaceSet takes the structure returned by VaultPolicies and
+// returns a set of required namespaces
+func VaultNamespaceSet(policies map[string]map[string]*Vault) []string {
+ set := make(map[string]struct{})
+
+ for _, tgp := range policies {
+ for _, tp := range tgp {
+ if tp.Namespace != "" {
+ set[tp.Namespace] = struct{}{}
+ }
+ }
+ }
+
+ flattened := make([]string, 0, len(set))
+ for p := range set {
+ flattened = append(flattened, p)
+ }
+ return flattened
+}
+
// DenormalizeAllocationJobs is used to attach a job to all allocations that are
// non-terminal and do not have a job already. This is useful in cases where the
// job is normalized.
diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go
index 61ad06e325b..7ad54958c5d 100644
--- a/nomad/structs/structs.go
+++ b/nomad/structs/structs.go
@@ -3804,6 +3804,9 @@ type Job struct {
// transfer the token and is not stored after Job submission.
VaultToken string
+ // VaultNamespace is the Vault namepace
+ VaultNamespace string
+
// NomadTokenID is the Accessor ID of the ACL token (if any)
// used to register this version of the job. Used by deploymentwatcher.
NomadTokenID string
@@ -7973,6 +7976,9 @@ type Vault struct {
// Policies is the set of policies that the task needs access to
Policies []string
+ // Namespace is the vault namespace that should be used.
+ Namespace string
+
// Env marks whether the Vault Token should be exposed as an environment
// variable
Env bool
diff --git a/nomad/vault.go b/nomad/vault.go
index 013f38be921..6f23a47f5b0 100644
--- a/nomad/vault.go
+++ b/nomad/vault.go
@@ -160,12 +160,13 @@ type PurgeVaultAccessorFn func(accessors []*structs.VaultAccessor) error
// tokenData holds the relevant information about the Vault token passed to the
// client.
type tokenData struct {
- CreationTTL int `mapstructure:"creation_ttl"`
- TTL int `mapstructure:"ttl"`
- Renewable bool `mapstructure:"renewable"`
- Policies []string `mapstructure:"policies"`
- Role string `mapstructure:"role"`
- Root bool
+ CreationTTL int `mapstructure:"creation_ttl"`
+ TTL int `mapstructure:"ttl"`
+ Renewable bool `mapstructure:"renewable"`
+ Policies []string `mapstructure:"policies"`
+ Role string `mapstructure:"role"`
+ NamespacePath string `mapstructure:"namespace_path"`
+ Root bool
}
// vaultClient is the Servers implementation of the VaultClient interface. The
@@ -240,11 +241,17 @@ type vaultClient struct {
// setConfigLock serializes access to the SetConfig method
setConfigLock sync.Mutex
+
+ entHandler taskClientHandler
+}
+
+type taskClientHandler interface {
+ clientForTask(v *vaultClient, namespace string) (*vapi.Client, error)
}
// NewVaultClient returns a Vault client from the given config. If the client
// couldn't be made an error is returned.
-func NewVaultClient(c *config.VaultConfig, logger log.Logger, purgeFn PurgeVaultAccessorFn) (*vaultClient, error) {
+func NewVaultClient(c *config.VaultConfig, logger log.Logger, purgeFn PurgeVaultAccessorFn, delegate taskClientHandler) (*vaultClient, error) {
if c == nil {
return nil, fmt.Errorf("must pass valid VaultConfig")
}
@@ -255,14 +262,18 @@ func NewVaultClient(c *config.VaultConfig, logger log.Logger, purgeFn PurgeVault
if purgeFn == nil {
purgeFn = func(accessors []*structs.VaultAccessor) error { return nil }
}
+ if delegate == nil {
+ delegate = &VaultNoopDelegate{}
+ }
v := &vaultClient{
- config: c,
- logger: logger.Named("vault"),
- limiter: rate.NewLimiter(requestRateLimit, int(requestRateLimit)),
- revoking: make(map[*structs.VaultAccessor]time.Time),
- purgeFn: purgeFn,
- tomb: &tomb.Tomb{},
+ config: c,
+ logger: logger.Named("vault"),
+ limiter: rate.NewLimiter(requestRateLimit, int(requestRateLimit)),
+ revoking: make(map[*structs.VaultAccessor]time.Time),
+ purgeFn: purgeFn,
+ tomb: &tomb.Tomb{},
+ entHandler: delegate,
}
if v.config.IsEnabled() {
@@ -945,7 +956,6 @@ func (v *vaultClient) CreateToken(ctx context.Context, a *structs.Allocation, ta
if !v.Active() {
return nil, structs.NewRecoverableError(fmt.Errorf("Vault client not active"), true)
}
-
// Check if we have established a connection with Vault
if established, err := v.ConnectionEstablished(); !established && err == nil {
return nil, structs.NewRecoverableError(fmt.Errorf("Connection to Vault has not been established"), true)
@@ -970,6 +980,12 @@ func (v *vaultClient) CreateToken(ctx context.Context, a *structs.Allocation, ta
return nil, fmt.Errorf("Task does not require Vault policies")
}
+ // Set namespace for task
+ namespaceForTask := v.config.Namespace
+ if taskVault.Namespace != "" {
+ namespaceForTask = taskVault.Namespace
+ }
+
// Build the creation request
req := &vapi.TokenCreateRequest{
Policies: taskVault.Policies,
@@ -977,6 +993,7 @@ func (v *vaultClient) CreateToken(ctx context.Context, a *structs.Allocation, ta
"AllocationID": a.ID,
"Task": task,
"NodeID": a.NodeID,
+ "Namespace": namespaceForTask,
},
TTL: v.childTTL,
DisplayName: fmt.Sprintf("%s-%s", a.ID, task),
@@ -992,12 +1009,19 @@ func (v *vaultClient) CreateToken(ctx context.Context, a *structs.Allocation, ta
var secret *vapi.Secret
var err error
role := v.getRole()
+
+ // Fetch client for task
+ taskClient, err := v.entHandler.clientForTask(v, namespaceForTask)
+ if err != nil {
+ return nil, err
+ }
+
if v.tokenData.Root && role == "" {
req.Period = v.childTTL
- secret, err = v.auth.Create(req)
+ secret, err = taskClient.Auth().Token().Create(req)
} else {
// Make the token using the role
- secret, err = v.auth.CreateWithRole(req, v.getRole())
+ secret, err = taskClient.Auth().Token().CreateWithRole(req, v.getRole())
}
// Determine whether it is unrecoverable
@@ -1064,6 +1088,22 @@ func PoliciesFrom(s *vapi.Secret) ([]string, error) {
return s.TokenPolicies()
}
+// PolicyDataFrom parses the Data returned by a token lookup.
+// It should not be used to parse TokenPolicies as the list will not be
+// exhaustive.
+func PolicyDataFrom(s *vapi.Secret) (tokenData, error) {
+ if s == nil {
+ return tokenData{}, fmt.Errorf("cannot parse nil Vault secret")
+ }
+ var data tokenData
+
+ if err := mapstructure.WeakDecode(s.Data, &data); err != nil {
+ return tokenData{}, fmt.Errorf("failed to parse Vault token's data block: %v", err)
+ }
+
+ return data, nil
+}
+
// RevokeTokens revokes the passed set of accessors. If committed is set, the
// purge function passed to the client is called. If there is an error purging
// either because of Vault failures or because of the purge function, the
@@ -1385,3 +1425,10 @@ func (v *vaultClient) extendExpiration(ttlSeconds int) {
v.currentExpiration = time.Now().Add(time.Duration(ttlSeconds) * time.Second)
v.currentExpirationLock.Unlock()
}
+
+// VaultVaultNoopDelegate returns the default vault api auth token handler
+type VaultNoopDelegate struct{}
+
+func (e *VaultNoopDelegate) clientForTask(v *vaultClient, namespace string) (*vapi.Client, error) {
+ return v.client, nil
+}
diff --git a/nomad/vault_test.go b/nomad/vault_test.go
index 3f94d79446e..cd7f3d13d73 100644
--- a/nomad/vault_test.go
+++ b/nomad/vault_test.go
@@ -157,20 +157,20 @@ func TestVaultClient_BadConfig(t *testing.T) {
logger := testlog.HCLogger(t)
// Should be no error since Vault is not enabled
- _, err := NewVaultClient(nil, logger, nil)
+ _, err := NewVaultClient(nil, logger, nil, nil)
if err == nil || !strings.Contains(err.Error(), "valid") {
t.Fatalf("expected config error: %v", err)
}
tr := true
conf.Enabled = &tr
- _, err = NewVaultClient(conf, logger, nil)
+ _, err = NewVaultClient(conf, logger, nil, nil)
if err == nil || !strings.Contains(err.Error(), "token must be set") {
t.Fatalf("Expected token unset error: %v", err)
}
conf.Token = "123"
- _, err = NewVaultClient(conf, logger, nil)
+ _, err = NewVaultClient(conf, logger, nil, nil)
if err == nil || !strings.Contains(err.Error(), "address must be set") {
t.Fatalf("Expected address unset error: %v", err)
}
@@ -192,7 +192,7 @@ func TestVaultClient_WithNamespaceSupport(t *testing.T) {
logger := testlog.HCLogger(t)
// Should be no error since Vault is not enabled
- c, err := NewVaultClient(conf, logger, nil)
+ c, err := NewVaultClient(conf, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
@@ -217,7 +217,7 @@ func TestVaultClient_WithoutNamespaceSupport(t *testing.T) {
logger := testlog.HCLogger(t)
// Should be no error since Vault is not enabled
- c, err := NewVaultClient(conf, logger, nil)
+ c, err := NewVaultClient(conf, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
@@ -236,7 +236,7 @@ func TestVaultClient_EstablishConnection(t *testing.T) {
v := testutil.NewTestVaultDelayed(t)
logger := testlog.HCLogger(t)
v.Config.ConnectionRetryIntv = 100 * time.Millisecond
- client, err := NewVaultClient(v.Config, logger, nil)
+ client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
@@ -304,7 +304,7 @@ func TestVaultClient_ValidateRole(t *testing.T) {
logger := testlog.HCLogger(t)
v.Config.ConnectionRetryIntv = 100 * time.Millisecond
- client, err := NewVaultClient(v.Config, logger, nil)
+ client, err := NewVaultClient(v.Config, logger, nil, nil)
require.NoError(t, err)
defer client.Stop()
@@ -353,7 +353,7 @@ func TestVaultClient_ValidateRole_Success(t *testing.T) {
logger := testlog.HCLogger(t)
v.Config.ConnectionRetryIntv = 100 * time.Millisecond
- client, err := NewVaultClient(v.Config, logger, nil)
+ client, err := NewVaultClient(v.Config, logger, nil, nil)
require.NoError(t, err)
defer client.Stop()
@@ -399,7 +399,7 @@ func TestVaultClient_ValidateRole_Deprecated_Success(t *testing.T) {
logger := testlog.HCLogger(t)
v.Config.ConnectionRetryIntv = 100 * time.Millisecond
- client, err := NewVaultClient(v.Config, logger, nil)
+ client, err := NewVaultClient(v.Config, logger, nil, nil)
require.NoError(t, err)
defer client.Stop()
@@ -433,7 +433,7 @@ func TestVaultClient_ValidateRole_NonExistent(t *testing.T) {
logger := testlog.HCLogger(t)
v.Config.ConnectionRetryIntv = 100 * time.Millisecond
v.Config.Role = "test-nonexistent"
- client, err := NewVaultClient(v.Config, logger, nil)
+ client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
@@ -481,7 +481,7 @@ func TestVaultClient_ValidateToken(t *testing.T) {
logger := testlog.HCLogger(t)
v.Config.ConnectionRetryIntv = 100 * time.Millisecond
- client, err := NewVaultClient(v.Config, logger, nil)
+ client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
@@ -523,7 +523,7 @@ func TestVaultClient_SetActive(t *testing.T) {
defer v.Stop()
logger := testlog.HCLogger(t)
- client, err := NewVaultClient(v.Config, logger, nil)
+ client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
@@ -559,7 +559,7 @@ func TestVaultClient_SetConfig(t *testing.T) {
v2.Config.Token = defaultTestVaultWhitelistRoleAndToken(v2, t, 20)
logger := testlog.HCLogger(t)
- client, err := NewVaultClient(v.Config, logger, nil)
+ client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
@@ -622,7 +622,7 @@ func TestVaultClient_SetConfig_Deadlock(t *testing.T) {
v2.Config.Token = defaultTestVaultWhitelistRoleAndToken(v2, t, 20)
logger := testlog.HCLogger(t)
- client, err := NewVaultClient(v.Config, logger, nil)
+ client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
@@ -647,7 +647,7 @@ func TestVaultClient_SetConfig_Disable(t *testing.T) {
defer v.Stop()
logger := testlog.HCLogger(t)
- client, err := NewVaultClient(v.Config, logger, nil)
+ client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
@@ -685,7 +685,7 @@ func TestVaultClient_RenewalLoop(t *testing.T) {
// Start the client
logger := testlog.HCLogger(t)
- client, err := NewVaultClient(v.Config, logger, nil)
+ client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
@@ -721,7 +721,7 @@ func TestVaultClientRenewUpdatesExpiration(t *testing.T) {
// Start the client
logger := testlog.HCLogger(t)
- client, err := NewVaultClient(v.Config, logger, nil)
+ client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
@@ -760,7 +760,7 @@ func TestVaultClient_StopsAfterPermissionError(t *testing.T) {
// Start the client
logger := testlog.HCLogger(t)
- client, err := NewVaultClient(v.Config, logger, nil)
+ client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
@@ -794,7 +794,7 @@ func TestVaultClient_LoopsUntilCannotRenew(t *testing.T) {
// Start the client
logger := testlog.HCLogger(t)
- client, err := NewVaultClient(v.Config, logger, nil)
+ client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
@@ -856,7 +856,7 @@ func TestVaultClient_LookupToken_Invalid(t *testing.T) {
// Enable vault but use a bad address so it never establishes a conn
logger := testlog.HCLogger(t)
- client, err := NewVaultClient(conf, logger, nil)
+ client, err := NewVaultClient(conf, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
@@ -875,7 +875,7 @@ func TestVaultClient_LookupToken_Root(t *testing.T) {
defer v.Stop()
logger := testlog.HCLogger(t)
- client, err := NewVaultClient(v.Config, logger, nil)
+ client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
@@ -940,7 +940,7 @@ func TestVaultClient_LookupToken_Role(t *testing.T) {
v.Config.Token = defaultTestVaultWhitelistRoleAndToken(v, t, 5)
logger := testlog.HCLogger(t)
- client, err := NewVaultClient(v.Config, logger, nil)
+ client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
@@ -1002,7 +1002,7 @@ func TestVaultClient_LookupToken_RateLimit(t *testing.T) {
defer v.Stop()
logger := testlog.HCLogger(t)
- client, err := NewVaultClient(v.Config, logger, nil)
+ client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
@@ -1062,7 +1062,7 @@ func TestVaultClient_CreateToken_Root(t *testing.T) {
defer v.Stop()
logger := testlog.HCLogger(t)
- client, err := NewVaultClient(v.Config, logger, nil)
+ client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
@@ -1110,7 +1110,7 @@ func TestVaultClient_CreateToken_Whitelist_Role(t *testing.T) {
// Start the client
logger := testlog.HCLogger(t)
- client, err := NewVaultClient(v.Config, logger, nil)
+ client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
@@ -1161,7 +1161,7 @@ func TestVaultClient_CreateToken_Root_Target_Role(t *testing.T) {
// Start the client
logger := testlog.HCLogger(t)
- client, err := NewVaultClient(v.Config, logger, nil)
+ client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
@@ -1220,7 +1220,7 @@ func TestVaultClient_CreateToken_Blacklist_Role(t *testing.T) {
// Start the client
logger := testlog.HCLogger(t)
- client, err := NewVaultClient(v.Config, logger, nil)
+ client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
@@ -1269,7 +1269,7 @@ func TestVaultClient_CreateToken_Role_InvalidToken(t *testing.T) {
// Start the client
logger := testlog.HCLogger(t)
- client, err := NewVaultClient(v.Config, logger, nil)
+ client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
@@ -1307,7 +1307,7 @@ func TestVaultClient_CreateToken_Role_Unrecoverable(t *testing.T) {
// Start the client
logger := testlog.HCLogger(t)
- client, err := NewVaultClient(v.Config, logger, nil)
+ client, err := NewVaultClient(v.Config, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
@@ -1341,7 +1341,7 @@ func TestVaultClient_CreateToken_Prestart(t *testing.T) {
}
logger := testlog.HCLogger(t)
- client, err := NewVaultClient(vconfig, logger, nil)
+ client, err := NewVaultClient(vconfig, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
@@ -1372,7 +1372,7 @@ func TestVaultClient_MarkForRevocation(t *testing.T) {
Addr: "http://127.0.0.1:0",
}
logger := testlog.HCLogger(t)
- client, err := NewVaultClient(vconfig, logger, nil)
+ client, err := NewVaultClient(vconfig, logger, nil, nil)
require.NoError(t, err)
client.SetActive(true)
@@ -1400,7 +1400,7 @@ func TestVaultClient_RevokeTokens_PreEstablishs(t *testing.T) {
Addr: "http://127.0.0.1:0",
}
logger := testlog.HCLogger(t)
- client, err := NewVaultClient(vconfig, logger, nil)
+ client, err := NewVaultClient(vconfig, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
@@ -1446,7 +1446,7 @@ func TestVaultClient_RevokeTokens_Failures_TTL(t *testing.T) {
Addr: "http://127.0.0.1:0",
}
logger := testlog.HCLogger(t)
- client, err := NewVaultClient(vconfig, logger, nil)
+ client, err := NewVaultClient(vconfig, logger, nil, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
@@ -1494,7 +1494,7 @@ func TestVaultClient_RevokeTokens_Root(t *testing.T) {
}
logger := testlog.HCLogger(t)
- client, err := NewVaultClient(v.Config, logger, purge)
+ client, err := NewVaultClient(v.Config, logger, purge, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
@@ -1562,7 +1562,7 @@ func TestVaultClient_RevokeTokens_Role(t *testing.T) {
}
logger := testlog.HCLogger(t)
- client, err := NewVaultClient(v.Config, logger, purge)
+ client, err := NewVaultClient(v.Config, logger, purge, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
@@ -1633,7 +1633,7 @@ func TestVaultClient_RevokeTokens_Idempotent(t *testing.T) {
}
logger := testlog.HCLogger(t)
- client, err := NewVaultClient(v.Config, logger, purge)
+ client, err := NewVaultClient(v.Config, logger, purge, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}
diff --git a/vendor/github.com/hashicorp/nomad/api/jobs.go b/vendor/github.com/hashicorp/nomad/api/jobs.go
index 8308f0a7fc6..bd79537252f 100644
--- a/vendor/github.com/hashicorp/nomad/api/jobs.go
+++ b/vendor/github.com/hashicorp/nomad/api/jobs.go
@@ -787,6 +787,7 @@ type Job struct {
Meta map[string]string
ConsulToken *string `mapstructure:"consul_token"`
VaultToken *string `mapstructure:"vault_token"`
+ VaultNamespace *string `mapstructure:"vault_namespace"`
NomadTokenID *string `mapstructure:"nomad_token_id"`
Status *string
StatusDescription *string
@@ -850,6 +851,9 @@ func (j *Job) Canonicalize() {
if j.VaultToken == nil {
j.VaultToken = stringToPtr("")
}
+ if j.VaultNamespace == nil {
+ j.VaultNamespace = stringToPtr("")
+ }
if j.NomadTokenID == nil {
j.NomadTokenID = stringToPtr("")
}
diff --git a/vendor/github.com/hashicorp/nomad/api/tasks.go b/vendor/github.com/hashicorp/nomad/api/tasks.go
index b9b79af5470..8275813e6ee 100644
--- a/vendor/github.com/hashicorp/nomad/api/tasks.go
+++ b/vendor/github.com/hashicorp/nomad/api/tasks.go
@@ -812,6 +812,7 @@ func (tmpl *Template) Canonicalize() {
type Vault struct {
Policies []string
+ Namespace *string `mapstructure:"namespace"`
Env *bool
ChangeMode *string `mapstructure:"change_mode"`
ChangeSignal *string `mapstructure:"change_signal"`
@@ -821,6 +822,9 @@ func (v *Vault) Canonicalize() {
if v.Env == nil {
v.Env = boolToPtr(true)
}
+ if v.Namespace == nil {
+ v.Namespace = stringToPtr("")
+ }
if v.ChangeMode == nil {
v.ChangeMode = stringToPtr("restart")
}
diff --git a/website/pages/docs/job-specification/vault.mdx b/website/pages/docs/job-specification/vault.mdx
index 996f3641abd..f823659c5c9 100644
--- a/website/pages/docs/job-specification/vault.mdx
+++ b/website/pages/docs/job-specification/vault.mdx
@@ -71,6 +71,10 @@ with Vault as well.
- `env` `(bool: true)` - Specifies if the `VAULT_TOKEN` and `VAULT_NAMESPACE`
environment variables should be set when starting the task.
+- `namespace` `(string: "")` - Specifies the Vault Namespace
+ to use for the task. The Nomad client will retrieve a Vault token that is scoped to
+ this particular namespace.
+
- `policies` `(array: [])` - Specifies the set of Vault policies that
the task requires. The Nomad client will retrieve a Vault token that is
limited to those policies.
@@ -106,6 +110,22 @@ vault {
}
```
+### Vault Namespace
+
+This example shows specifying a particular Vault namespace for a given task.
+
+
+
+```hcl
+vault {
+ policies = ["frontend"]
+ namespace = "engineering/frontend"
+
+ change_mode = "signal"
+ change_signal = "SIGINT"
+}
+```
+
[restart]: /docs/job-specification/restart 'Nomad restart Job Specification'
[template]: /docs/job-specification/template 'Nomad template Job Specification'
[vault]: https://www.vaultproject.io/ 'Vault by HashiCorp'
diff --git a/website/pages/docs/upgrade/upgrade-specific.mdx b/website/pages/docs/upgrade/upgrade-specific.mdx
index d26629a6585..3d3b315767c 100644
--- a/website/pages/docs/upgrade/upgrade-specific.mdx
+++ b/website/pages/docs/upgrade/upgrade-specific.mdx
@@ -15,6 +15,16 @@ details provided for their upgrades as a result of new features or changed
behavior. This page is used to document those details separately from the
standard upgrade flow.
+## Nomad 0.12.2
+
+### Vault Namespace Environment Variable
+
+Nomad 0.12.2 allows Enterprise users to specify a Vault Namespace for a
+particular Job, Group, or Task. Before Nomad 0.12.2 the `VAULT_NAMESPACE`
+environment variable was ignored when submitting jobs. Nomad 0.12.2 changes the
+functionality and uses the `VAULT_NAMESPACE` environment variable when
+submitting jobs.
+
## Nomad 0.12.0
### Enterprise Licensing