mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
secrets: add vault secrets provider (#26198)
This commit is contained in:
91
client/allocrunner/taskrunner/secrets/vault_provider.go
Normal file
91
client/allocrunner/taskrunner/secrets/vault_provider.go
Normal file
@@ -0,0 +1,91 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/hashicorp/go-envparse"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
const (
|
||||
VAULT_KV = "kv"
|
||||
VAULT_KV_V2 = "kv_v2"
|
||||
)
|
||||
|
||||
type vaultProviderConfig struct {
|
||||
Engine string `mapstructure:"engine"`
|
||||
}
|
||||
|
||||
func defaultVaultConfig() *vaultProviderConfig {
|
||||
return &vaultProviderConfig{
|
||||
Engine: VAULT_KV,
|
||||
}
|
||||
}
|
||||
|
||||
type VaultProvider struct {
|
||||
secret *structs.Secret
|
||||
tmplPath string
|
||||
conf *vaultProviderConfig
|
||||
}
|
||||
|
||||
// NewVaultProvider takes a task secret and decodes the config, overwriting the default config fields
|
||||
// with any provided fields, returning an error if the secret or secret's config is invalid.
|
||||
func NewVaultProvider(s *structs.Secret, path string) (*VaultProvider, error) {
|
||||
if s == nil {
|
||||
return nil, fmt.Errorf("empty secret for vault provider")
|
||||
}
|
||||
|
||||
conf := defaultVaultConfig()
|
||||
if err := mapstructure.Decode(s.Config, conf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &VaultProvider{
|
||||
secret: s,
|
||||
tmplPath: path,
|
||||
conf: conf,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (v *VaultProvider) BuildTemplate() *structs.Template {
|
||||
indexKey := ".Data"
|
||||
if v.conf.Engine == VAULT_KV_V2 {
|
||||
indexKey = ".Data.data"
|
||||
}
|
||||
|
||||
data := fmt.Sprintf(`
|
||||
{{ with secret "%s" }}
|
||||
{{ range $k, $v := %s }}
|
||||
secret.%s.{{ $k }}={{ $v }}
|
||||
{{ end }}
|
||||
{{ end }}`,
|
||||
v.secret.Path, indexKey, v.secret.Name)
|
||||
|
||||
return &structs.Template{
|
||||
EmbeddedTmpl: data,
|
||||
DestPath: v.tmplPath,
|
||||
ChangeMode: structs.TemplateChangeModeNoop,
|
||||
Once: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *VaultProvider) Parse() (map[string]string, error) {
|
||||
// we checked escape before we rendered the file
|
||||
dest := filepath.Clean(v.tmplPath)
|
||||
f, err := os.Open(dest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error opening env template: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
f.Close()
|
||||
os.Remove(dest)
|
||||
}()
|
||||
|
||||
return envparse.Parse(f)
|
||||
}
|
||||
100
client/allocrunner/taskrunner/secrets/vault_provider_test.go
Normal file
100
client/allocrunner/taskrunner/secrets/vault_provider_test.go
Normal file
@@ -0,0 +1,100 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/shoenig/test/must"
|
||||
)
|
||||
|
||||
func TestVaultProvider_BuildTemplate(t *testing.T) {
|
||||
t.Run("kv template succeeds", func(t *testing.T) {
|
||||
testDir := t.TempDir()
|
||||
testSecret := &structs.Secret{
|
||||
Name: "foo",
|
||||
Provider: "vault",
|
||||
Path: "/test/path",
|
||||
}
|
||||
p, err := NewVaultProvider(testSecret, testDir)
|
||||
must.NoError(t, err)
|
||||
|
||||
tmpl := p.BuildTemplate()
|
||||
must.NotNil(t, tmpl)
|
||||
|
||||
// expected template should have correct path, index, and name
|
||||
expectedTmpl := `
|
||||
{{ with secret "/test/path" }}
|
||||
{{ range $k, $v := .Data }}
|
||||
secret.foo.{{ $k }}={{ $v }}
|
||||
{{ end }}
|
||||
{{ end }}`
|
||||
// validate template string contains expected data
|
||||
must.Eq(t, tmpl.EmbeddedTmpl, expectedTmpl)
|
||||
})
|
||||
|
||||
t.Run("kv_v2 template succeeds", func(t *testing.T) {
|
||||
testDir := t.TempDir()
|
||||
testSecret := &structs.Secret{
|
||||
Name: "foo",
|
||||
Provider: "vault",
|
||||
Path: "/test/path",
|
||||
Config: map[string]any{
|
||||
"engine": VAULT_KV_V2,
|
||||
},
|
||||
}
|
||||
p, err := NewVaultProvider(testSecret, testDir)
|
||||
must.NoError(t, err)
|
||||
|
||||
tmpl := p.BuildTemplate()
|
||||
must.NotNil(t, tmpl)
|
||||
|
||||
// expected template should have correct path, index, and name
|
||||
expectedTmpl := `
|
||||
{{ with secret "/test/path" }}
|
||||
{{ range $k, $v := .Data.data }}
|
||||
secret.foo.{{ $k }}={{ $v }}
|
||||
{{ end }}
|
||||
{{ end }}`
|
||||
// validate template string contains expected data
|
||||
must.Eq(t, tmpl.EmbeddedTmpl, expectedTmpl)
|
||||
})
|
||||
|
||||
t.Run("invalid config options errors", func(t *testing.T) {
|
||||
testDir := t.TempDir()
|
||||
testSecret := &structs.Secret{
|
||||
Name: "foo",
|
||||
Provider: "vault",
|
||||
Path: "/test/path",
|
||||
Config: map[string]any{
|
||||
"engine": 123,
|
||||
},
|
||||
}
|
||||
_, err := NewVaultProvider(testSecret, testDir)
|
||||
must.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestVaultProvider_Parse(t *testing.T) {
|
||||
testDir := t.TempDir()
|
||||
|
||||
tmplPath := filepath.Join(testDir, "foo")
|
||||
|
||||
data := "foo=bar"
|
||||
err := os.WriteFile(tmplPath, []byte(data), 0777)
|
||||
must.NoError(t, err)
|
||||
|
||||
p, err := NewVaultProvider(&structs.Secret{}, tmplPath)
|
||||
must.NoError(t, err)
|
||||
|
||||
vars, err := p.Parse()
|
||||
must.NoError(t, err)
|
||||
must.Eq(t, vars, map[string]string{"foo": "bar"})
|
||||
|
||||
_, err = os.Stat(tmplPath)
|
||||
must.ErrorContains(t, err, "no such file")
|
||||
}
|
||||
@@ -173,7 +173,11 @@ func (h *secretsHook) buildSecretProviders(secretDir string) ([]SecretProvider,
|
||||
providers = append(providers, p)
|
||||
}
|
||||
case "vault":
|
||||
// Unimplemented
|
||||
if p, err := secrets.NewVaultProvider(s, tmplPath); err != nil {
|
||||
multierror.Append(mErr, err)
|
||||
} else {
|
||||
providers = append(providers, p)
|
||||
}
|
||||
default:
|
||||
multierror.Append(mErr, fmt.Errorf("unknown secret provider type: %s", s.Provider))
|
||||
}
|
||||
|
||||
@@ -19,9 +19,11 @@ import (
|
||||
"github.com/hashicorp/nomad/client/config"
|
||||
"github.com/hashicorp/nomad/client/taskenv"
|
||||
"github.com/hashicorp/nomad/helper/bufconndialer"
|
||||
"github.com/hashicorp/nomad/helper/pointer"
|
||||
"github.com/hashicorp/nomad/helper/testlog"
|
||||
"github.com/hashicorp/nomad/nomad/mock"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
structsc "github.com/hashicorp/nomad/nomad/structs/config"
|
||||
"github.com/shoenig/test/must"
|
||||
)
|
||||
|
||||
@@ -215,3 +217,84 @@ func TestSecretsHook_Prestart_Nomad(t *testing.T) {
|
||||
must.Eq(t, expected, secretHook.taskSecrets)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSecretsHook_Prestart_Vault(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
secretsResp := `
|
||||
{
|
||||
"Data": {
|
||||
"data": {
|
||||
"secret": "secret"
|
||||
},
|
||||
"metadata": {
|
||||
"created_time": "2023-10-18T15:58:29.65137Z",
|
||||
"custom_metadata": null,
|
||||
"deletion_time": "",
|
||||
"destroyed": false,
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
// Start test server to simulate Vault cluster responses.
|
||||
// reqCh := make(chan any)
|
||||
defaultVaultServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, secretsResp)
|
||||
}))
|
||||
t.Cleanup(defaultVaultServer.Close)
|
||||
|
||||
// Setup client with Vault config.
|
||||
clientConfig := config.DefaultConfig()
|
||||
clientConfig.TemplateConfig.DisableSandbox = true
|
||||
clientConfig.VaultConfigs = map[string]*structsc.VaultConfig{
|
||||
structs.VaultDefaultCluster: {
|
||||
Name: structs.VaultDefaultCluster,
|
||||
Enabled: pointer.Of(true),
|
||||
Addr: defaultVaultServer.URL,
|
||||
},
|
||||
}
|
||||
|
||||
taskDir := t.TempDir()
|
||||
alloc := mock.MinAlloc()
|
||||
task := alloc.Job.TaskGroups[0].Tasks[0]
|
||||
|
||||
conf := &secretsHookConfig{
|
||||
|
||||
// alloc: alloc,
|
||||
logger: testlog.HCLogger(t),
|
||||
lifecycle: trtesting.NewMockTaskHooks(),
|
||||
events: &trtesting.MockEmitter{},
|
||||
clientConfig: clientConfig,
|
||||
envBuilder: taskenv.NewBuilder(mock.Node(), alloc, task, clientConfig.Region),
|
||||
}
|
||||
secretHook := newSecretsHook(conf, []*structs.Secret{
|
||||
{
|
||||
Name: "test_secret",
|
||||
Provider: "vault",
|
||||
Path: "/test/path",
|
||||
Config: map[string]any{
|
||||
"engine": "kv_v2",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Start template hook with a timeout context to ensure it exists.
|
||||
req := &interfaces.TaskPrestartRequest{
|
||||
Alloc: alloc,
|
||||
Task: task,
|
||||
TaskDir: &allocdir.TaskDir{Dir: taskDir, SecretsDir: taskDir},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
err := secretHook.Prestart(ctx, req, nil)
|
||||
must.NoError(t, err)
|
||||
|
||||
exp := map[string]string{
|
||||
"secret.test_secret.secret": "secret",
|
||||
}
|
||||
|
||||
must.Eq(t, exp, secretHook.taskSecrets)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user