secrets: add common secrets plugins impl (#26335)

Co-authored-by: Michael Schurter <mschurter@hashicorp.com>
This commit is contained in:
Michael Smithhisler
2025-07-31 10:14:52 -04:00
parent c7a6b8b253
commit 00ef9cacab
12 changed files with 638 additions and 16 deletions

View File

@@ -11,6 +11,7 @@
"client/allocdir/...", "client/allocdir/...",
"client/allochealth/...", "client/allochealth/...",
"client/allocwatcher/...", "client/allocwatcher/...",
"client/commonplugins/...",
"client/config/...", "client/config/...",
"client/consul/...", "client/consul/...",
"client/devicemanager/...", "client/devicemanager/...",

View File

@@ -0,0 +1,65 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package secrets
import (
"context"
"errors"
"fmt"
"github.com/hashicorp/nomad/client/commonplugins"
)
type ExternalPluginProvider struct {
// plugin is the commonplugin to be executed by this secret
plugin commonplugins.SecretsPlugin
// response is the plugin response saved after Fetch is called
response *commonplugins.SecretResponse
// name of the plugin and also the executable
name string
// path is the secret location used in Fetch
path string
}
type Response struct {
Result map[string]string `json:"result"`
Error *string `json:"error,omitempty"`
}
func NewExternalPluginProvider(plugin commonplugins.SecretsPlugin, name string, path string) *ExternalPluginProvider {
return &ExternalPluginProvider{
plugin: plugin,
name: name,
path: path,
}
}
func (p *ExternalPluginProvider) Fetch(ctx context.Context) error {
resp, err := p.plugin.Fetch(ctx, p.path)
if err != nil {
return fmt.Errorf("failed to fetch secret from plugin %s: %w", p.name, err)
}
if resp.Error != nil {
return fmt.Errorf("error returned from secret plugin %s: %s", p.name, *resp.Error)
}
p.response = resp
return nil
}
func (p *ExternalPluginProvider) Parse() (map[string]string, error) {
if p.response == nil {
return nil, errors.New("no plugin response for provider to parse")
}
formatted := map[string]string{}
for k, v := range p.response.Result {
formatted[fmt.Sprintf("secret.%s.%s", p.name, k)] = v
}
return formatted, nil
}

View File

@@ -0,0 +1,92 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package secrets
import (
"context"
"errors"
"testing"
"github.com/hashicorp/nomad/client/commonplugins"
"github.com/shoenig/test/must"
"github.com/stretchr/testify/mock"
)
type MockSecretPlugin struct {
mock.Mock
}
func (m *MockSecretPlugin) Fingerprint(ctx context.Context) (*commonplugins.PluginFingerprint, error) {
return nil, nil
}
func (m *MockSecretPlugin) Fetch(ctx context.Context, path string) (*commonplugins.SecretResponse, error) {
args := m.Called()
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*commonplugins.SecretResponse), args.Error(1)
}
func (m *MockSecretPlugin) Parse() (map[string]string, error) {
return nil, nil
}
// SecretsPlugin is tested in commonplugins package. We can use a mock here to test how
// the ExternalPluginProvider handles various error scenarios when calling Fetch.
func TestExternalPluginProvider_Fetch(t *testing.T) {
t.Run("errors if fetch errors", func(t *testing.T) {
mockSecretPlugin := new(MockSecretPlugin)
mockSecretPlugin.On("Fetch", mock.Anything).Return(nil, errors.New("something bad"))
testProvider := NewExternalPluginProvider(mockSecretPlugin, "test", "test")
err := testProvider.Fetch(context.Background())
must.ErrorContains(t, err, "something bad")
})
t.Run("errors if fetch response contains error", func(t *testing.T) {
mockSecretPlugin := new(MockSecretPlugin)
testError := "something bad"
mockSecretPlugin.On("Fetch", mock.Anything).Return(&commonplugins.SecretResponse{
Result: nil,
Error: &testError,
}, nil)
testProvider := NewExternalPluginProvider(mockSecretPlugin, "test", "test")
err := testProvider.Fetch(context.Background())
must.ErrorContains(t, err, "error returned from secret plugin")
})
}
func TestExternalPluginProvider_Parse(t *testing.T) {
t.Run("formats response correctly", func(t *testing.T) {
testProvider := NewExternalPluginProvider(nil, "test", "test")
testProvider.response = &commonplugins.SecretResponse{
Result: map[string]string{
"testkey": "testvalue",
},
}
result, err := testProvider.Parse()
must.NoError(t, err)
exp := map[string]string{
"secret.test.testkey": "testvalue",
}
must.Eq(t, exp, result)
})
t.Run("errors if response is nil", func(t *testing.T) {
testProvider := NewExternalPluginProvider(nil, "test", "test")
testProvider.response = nil
result, err := testProvider.Parse()
must.Error(t, err)
must.Nil(t, result)
})
}

View File

@@ -13,6 +13,7 @@ import (
ti "github.com/hashicorp/nomad/client/allocrunner/taskrunner/interfaces" ti "github.com/hashicorp/nomad/client/allocrunner/taskrunner/interfaces"
"github.com/hashicorp/nomad/client/allocrunner/taskrunner/secrets" "github.com/hashicorp/nomad/client/allocrunner/taskrunner/secrets"
"github.com/hashicorp/nomad/client/allocrunner/taskrunner/template" "github.com/hashicorp/nomad/client/allocrunner/taskrunner/template"
"github.com/hashicorp/nomad/client/commonplugins"
"github.com/hashicorp/nomad/client/config" "github.com/hashicorp/nomad/client/config"
"github.com/hashicorp/nomad/client/taskenv" "github.com/hashicorp/nomad/client/taskenv"
"github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/nomad/structs"
@@ -22,14 +23,20 @@ import (
// work can modify this interface to include custom providers using a plugin // work can modify this interface to include custom providers using a plugin
// interface. // interface.
type SecretProvider interface { type SecretProvider interface {
// BuildTemplate should construct a template appropriate for that provider
// and append it to the templateManager's templates.
BuildTemplate() *structs.Template
// Parse allows each provider implementation to parse its "response" object. // Parse allows each provider implementation to parse its "response" object.
Parse() (map[string]string, error) Parse() (map[string]string, error)
} }
type TemplateProvider interface {
SecretProvider
BuildTemplate() *structs.Template
}
type PluginProvider interface {
SecretProvider
Fetch(context.Context) error
}
type secretsHookConfig struct { type secretsHookConfig struct {
// logger is used to log // logger is used to log
logger log.Logger logger log.Logger
@@ -98,7 +105,14 @@ func (h *secretsHook) Prestart(ctx context.Context, req *interfaces.TaskPrestart
} }
for _, p := range providers { for _, p := range providers {
templates = append(templates, p.BuildTemplate()) switch v := p.(type) {
case TemplateProvider:
templates = append(templates, v.BuildTemplate())
case PluginProvider:
if err := v.Fetch(ctx); err != nil {
return err
}
}
} }
vaultCluster := req.Task.GetVaultClusterName() vaultCluster := req.Task.GetVaultClusterName()
@@ -176,7 +190,12 @@ func (h *secretsHook) buildSecretProviders(secretDir string) ([]SecretProvider,
providers = append(providers, p) providers = append(providers, p)
} }
default: default:
multierror.Append(mErr, fmt.Errorf("unknown secret provider type: %s", s.Provider)) plug, err := commonplugins.NewExternalSecretsPlugin(h.clientConfig.CommonPluginDir, s.Provider)
if err != nil {
multierror.Append(mErr, err)
continue
}
providers = append(providers, secrets.NewExternalPluginProvider(plug, s.Name, s.Path))
} }
} }

View File

@@ -0,0 +1,63 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package commonplugins
import (
"bytes"
"context"
"errors"
"os/exec"
"syscall"
"time"
"github.com/hashicorp/go-version"
)
var (
ErrPluginNotExists error = errors.New("plugin not found")
ErrPluginNotExecutable error = errors.New("plugin not executable")
)
type CommonPlugin interface {
Fingerprint(ctx context.Context) (*PluginFingerprint, error)
}
// CommonPlugins are expected to respond to 'fingerprint' calls with json that
// unmarshals to this struct.
type PluginFingerprint struct {
Version *version.Version `json:"version"`
Type *string `json:"type"`
}
// runPlugin is a helper for executing the provided Cmd and capturing stdout/stderr.
// This helper implements both the soft and hard timeouts defined by the common
// plugins interface.
func runPlugin(cmd *exec.Cmd, killTimeout time.Duration) (stdout, stderr []byte, err error) {
var errBuf bytes.Buffer
cmd.Stderr = &errBuf
done := make(chan error, 1)
cmd.Cancel = func() error {
var cancelErr error
_ = cmd.Process.Signal(syscall.SIGTERM)
killTimer := time.NewTimer(killTimeout)
defer killTimer.Stop()
select {
case <-killTimer.C:
cancelErr = cmd.Process.Kill()
case <-done:
}
return cancelErr
}
// start the command
stdout, err = cmd.Output()
done <- err
stderr = errBuf.Bytes()
return
}

View File

@@ -0,0 +1,114 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package commonplugins
import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"time"
log "github.com/hashicorp/go-hclog"
"github.com/hashicorp/nomad/helper"
)
const (
SecretsPluginDir = "secrets"
// The timeout for the plugin command before it is send SIGTERM
SecretsCmdTimeout = 10 * time.Second
// The timeout before the command is sent SIGKILL after being SIGTERM'd
SecretsKillTimeout = 2 * time.Second
)
type SecretsPlugin interface {
CommonPlugin
Fetch(ctx context.Context, path string) (*SecretResponse, error)
}
type SecretResponse struct {
Result map[string]string `json:"result"`
Error *string `json:"error"`
}
type externalSecretsPlugin struct {
logger log.Logger
// pluginPath is the path on the host to the plugin executable
pluginPath string
}
func NewExternalSecretsPlugin(commonPluginDir string, name string) (*externalSecretsPlugin, error) {
executable := filepath.Join(commonPluginDir, SecretsPluginDir, name)
f, err := os.Stat(executable)
if err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf("%w: %q", ErrPluginNotExists, name)
}
return nil, err
}
if !helper.IsExecutable(f) {
return nil, fmt.Errorf("%w: %q", ErrPluginNotExecutable, name)
}
return &externalSecretsPlugin{
pluginPath: executable,
}, nil
}
func (e *externalSecretsPlugin) Fingerprint(ctx context.Context) (*PluginFingerprint, error) {
plugCtx, cancel := context.WithTimeout(ctx, SecretsCmdTimeout)
defer cancel()
cmd := exec.CommandContext(plugCtx, e.pluginPath, "fingerprint")
cmd.Env = []string{
"CPI_OPERATION=fingerprint",
}
stdout, stderr, err := runPlugin(cmd, SecretsKillTimeout)
if err != nil {
return nil, err
}
if len(stderr) > 0 {
e.logger.Info("fingerprint command stderr output", "msg", string(stderr))
}
res := &PluginFingerprint{}
if err := json.Unmarshal(stdout, &res); err != nil {
return nil, err
}
return res, nil
}
func (e *externalSecretsPlugin) Fetch(ctx context.Context, path string) (*SecretResponse, error) {
plugCtx, cancel := context.WithTimeout(ctx, SecretsCmdTimeout)
defer cancel()
cmd := exec.CommandContext(plugCtx, e.pluginPath, "fetch", path)
cmd.Env = []string{
"CPI_OPERATION=fetch",
}
stdout, stderr, err := runPlugin(cmd, SecretsKillTimeout)
if err != nil {
return nil, err
}
if len(stderr) > 0 {
e.logger.Info("fetch command stderr output", "msg", string(stderr))
}
res := &SecretResponse{}
if err := json.Unmarshal(stdout, &res); err != nil {
return nil, err
}
return res, nil
}

View File

@@ -0,0 +1,132 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package commonplugins
import (
"context"
"fmt"
"os"
"path/filepath"
"testing"
"time"
"github.com/hashicorp/nomad/ci"
"github.com/shoenig/test/must"
)
func TestExternalSecretsPlugin_Fingerprint(t *testing.T) {
ci.Parallel(t)
t.Run("runs successfully", func(t *testing.T) {
pluginDir, pluginName := setupTestPlugin(t, fmt.Appendf([]byte{}, "#!/bin/sh\ncat <<EOF\n%s\nEOF\n", `{"type": "secrets", "version": "1.0.0"}`))
plugin, err := NewExternalSecretsPlugin(pluginDir, pluginName)
must.NoError(t, err)
res, err := plugin.Fingerprint(context.Background())
must.NoError(t, err)
must.Eq(t, *res.Type, "secrets")
must.Eq(t, res.Version.String(), "1.0.0")
})
t.Run("errors on non-zero exit code", func(t *testing.T) {
pluginDir, pluginName := setupTestPlugin(t, fmt.Append([]byte{}, "#!/bin/sh\nexit 1\n"))
plugin, err := NewExternalSecretsPlugin(pluginDir, pluginName)
must.NoError(t, err)
res, err := plugin.Fingerprint(context.Background())
must.Error(t, err)
must.Nil(t, res)
})
t.Run("errors on timeout", func(t *testing.T) {
pluginDir, pluginName := setupTestPlugin(t, fmt.Appendf([]byte{}, "#!/bin/sh\nleep .5\n"))
plugin, err := NewExternalSecretsPlugin(pluginDir, pluginName)
must.NoError(t, err)
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
_, err = plugin.Fingerprint(ctx)
must.Error(t, err)
})
t.Run("errors on invalid json", func(t *testing.T) {
pluginDir, pluginName := setupTestPlugin(t, fmt.Append([]byte{}, "#!/bin/sh\ncat <<EOF\ninvalid\nEOF\n"))
plugin, err := NewExternalSecretsPlugin(pluginDir, pluginName)
must.NoError(t, err)
res, err := plugin.Fingerprint(context.Background())
must.Error(t, err)
must.Nil(t, res)
})
}
func TestExternalSecretsPlugin_Fetch(t *testing.T) {
ci.Parallel(t)
t.Run("runs successfully", func(t *testing.T) {
pluginDir, pluginName := setupTestPlugin(t, fmt.Appendf([]byte{}, "#!/bin/sh\ncat <<EOF\n%s\nEOF\n", `{"result": {"key": "value"}}`))
plugin, err := NewExternalSecretsPlugin(pluginDir, pluginName)
must.NoError(t, err)
res, err := plugin.Fetch(context.Background(), "test-path")
must.NoError(t, err)
exp := map[string]string{"key": "value"}
must.Eq(t, res.Result, exp)
})
t.Run("errors on non-zero exit code", func(t *testing.T) {
pluginDir, pluginName := setupTestPlugin(t, fmt.Append([]byte{}, "#!/bin/sh\nexit 1\n"))
plugin, err := NewExternalSecretsPlugin(pluginDir, pluginName)
must.NoError(t, err)
_, err = plugin.Fetch(context.Background(), "test-path")
must.Error(t, err)
})
t.Run("errors on timeout", func(t *testing.T) {
pluginDir, pluginName := setupTestPlugin(t, fmt.Append([]byte{}, "#!/bin/sh\nsleep .5\n"))
plugin, err := NewExternalSecretsPlugin(pluginDir, pluginName)
must.NoError(t, err)
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
_, err = plugin.Fetch(ctx, "dummy-path")
must.Error(t, err)
})
t.Run("errors on timeout", func(t *testing.T) {
pluginDir, pluginName := setupTestPlugin(t, fmt.Appendf([]byte{}, "#!/bin/sh\ncat <<EOF\n%s\nEOF\n", `invalid`))
plugin, err := NewExternalSecretsPlugin(pluginDir, pluginName)
must.NoError(t, err)
_, err = plugin.Fetch(context.Background(), "dummy-path")
must.Error(t, err)
})
}
func setupTestPlugin(t *testing.T, b []byte) (string, string) {
dir := t.TempDir()
plugin := "test-plugin"
// NewExternalSecretsPlugin expects the subdir "secrets" to be present
secretDir := filepath.Join(dir, SecretsPluginDir)
must.NoError(t, os.Mkdir(secretDir, 0755))
path := filepath.Join(secretDir, plugin)
must.NoError(t, os.WriteFile(path, b, 0755))
return dir, plugin
}

View File

@@ -340,6 +340,10 @@ type Config struct {
// HostNetworks is a map of the conigured host networks by name. // HostNetworks is a map of the conigured host networks by name.
HostNetworks map[string]*structs.ClientHostNetworkConfig HostNetworks map[string]*structs.ClientHostNetworkConfig
// CommonPluginDir is the root directory for plugins that implement
// the common plugin interface
CommonPluginDir string
// BindWildcardDefaultHostNetwork toggles if the default host network should accept all // BindWildcardDefaultHostNetwork toggles if the default host network should accept all
// destinations (true) or only filter on the IP of the default host network (false) when // destinations (true) or only filter on the IP of the default host network (false) when
// port mapping. This allows Nomad clients with no defined host networks to accept and // port mapping. This allows Nomad clients with no defined host networks to accept and

View File

@@ -43,7 +43,7 @@ var (
"nomad": NewNomadFingerprint, "nomad": NewNomadFingerprint,
"plugins_cni": NewPluginsCNIFingerprint, "plugins_cni": NewPluginsCNIFingerprint,
"host_volume_plugins": NewPluginsHostVolumeFingerprint, "host_volume_plugins": NewPluginsHostVolumeFingerprint,
"secrets": NewPluginsSecretsFingerprint, "secrets_plugins": NewPluginsSecretsFingerprint,
"signal": NewSignalFingerprint, "signal": NewSignalFingerprint,
"storage": NewStorageFingerprint, "storage": NewStorageFingerprint,
"vault": NewVaultFingerprint, "vault": NewVaultFingerprint,

View File

@@ -4,9 +4,15 @@
package fingerprint package fingerprint
import ( import (
"context"
"os"
"path/filepath"
"strings"
"time" "time"
"github.com/hashicorp/go-hclog" "github.com/hashicorp/go-hclog"
"github.com/hashicorp/nomad/client/commonplugins"
"github.com/hashicorp/nomad/helper"
) )
type SecretsPluginFingerprint struct { type SecretsPluginFingerprint struct {
@@ -21,13 +27,70 @@ func NewPluginsSecretsFingerprint(logger hclog.Logger) Fingerprint {
func (s *SecretsPluginFingerprint) Fingerprint(request *FingerprintRequest, response *FingerprintResponse) error { func (s *SecretsPluginFingerprint) Fingerprint(request *FingerprintRequest, response *FingerprintResponse) error {
// Add builtin secrets providers // Add builtin secrets providers
defer func() {
response.AddAttribute("plugins.secrets.nomad.version", "1.0.0") response.AddAttribute("plugins.secrets.nomad.version", "1.0.0")
response.AddAttribute("plugins.secrets.vault.version", "1.0.0") response.AddAttribute("plugins.secrets.vault.version", "1.0.0")
}()
response.Detected = true response.Detected = true
pluginDir := request.Config.CommonPluginDir
if pluginDir == "" {
return nil
}
secretsDir := filepath.Join(pluginDir, commonplugins.SecretsPluginDir)
files, err := helper.FindExecutableFiles(secretsDir)
if err != nil {
if os.IsNotExist(err) {
s.logger.Trace("secrets plugin dir does not exist", "dir", secretsDir)
} else {
s.logger.Warn("error finding secrets plugins", "dir", secretsDir, "error", err)
}
return nil // don't halt agent start
}
// map of plugin names to fingerprinted versions
plugins := map[string]string{}
for name := range files {
plug, err := commonplugins.NewExternalSecretsPlugin(request.Config.CommonPluginDir, name)
if err != nil {
return err
}
fprint, err := plug.Fingerprint(context.Background())
if err != nil {
s.logger.Error("secrets plugin failed fingerprint", "plugin", name, "error", err)
continue
}
if fprint.Version == nil || fprint.Type == nil {
continue
}
plugins[name] = fprint.Version.Original()
}
// if this was a reload, wipe what was there before
for k := range request.Node.Attributes {
if strings.HasPrefix(k, "plugins.secrets.") {
response.RemoveAttribute(k)
}
}
// set the attribute(s)
for plugin, version := range plugins {
s.logger.Debug("detected plugin", "plugin_id", plugin, "version", version)
response.AddAttribute("plugins.secrets."+plugin+".version", version)
}
return nil return nil
} }
func (s *SecretsPluginFingerprint) Periodic() (bool, time.Duration) { func (s *SecretsPluginFingerprint) Periodic() (bool, time.Duration) {
return false, 0 return false, 0
} }
func (s *SecretsPluginFingerprint) Reload() {
// secrets plugins are re-detected on agent reload
}

View File

@@ -4,21 +4,86 @@
package fingerprint package fingerprint
import ( import (
"os"
"path/filepath"
"runtime"
"testing" "testing"
"github.com/hashicorp/nomad/client/config"
"github.com/hashicorp/nomad/helper/testlog" "github.com/hashicorp/nomad/helper/testlog"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/shoenig/test/must" "github.com/shoenig/test/must"
) )
func TestPluginsSecretsFingerprint(t *testing.T) { func TestPluginsSecretsFingerprint(t *testing.T) {
fp := NewPluginsSecretsFingerprint(testlog.HCLogger(t)) fp := NewPluginsSecretsFingerprint(testlog.HCLogger(t))
node := &structs.Node{Attributes: map[string]string{}}
cfg := &config.Config{CommonPluginDir: ""}
req := &FingerprintRequest{Config: cfg, Node: node}
for name, path := range map[string]string{
"empty": "",
"non-existent": "/nowhere",
"impossible": "secrets_plugins_test.go",
} {
t.Run(name, func(t *testing.T) {
resp := FingerprintResponse{} resp := FingerprintResponse{}
err := fp.Fingerprint(&FingerprintRequest{}, &resp) cfg.CommonPluginDir = path
err := fp.Fingerprint(req, &resp)
must.NoError(t, err) must.NoError(t, err)
must.True(t, resp.Detected) must.True(t, resp.Detected) // always true due to "mkdir" built-in
must.MapContainsKeys(t, resp.Attributes, []string{
"plugins.secrets.nomad.version",
"plugins.secrets.vault.version",
}) })
}
if runtime.GOOS == "windows" {
t.Skip("test scripts not built for windows") // db TODO(1.10.0)
}
// happy path: dir exists. this one will contain a single valid plugin.
tmp := t.TempDir()
secretsDir := filepath.Join(tmp, "secrets")
os.Mkdir(secretsDir, 0777)
cfg.CommonPluginDir = tmp
files := []struct {
name string
contents string
perm os.FileMode
}{
// only this first one should be detected as a valid plugin
{"happy-plugin", "#!/usr/bin/env sh\necho '{\"type\": \"secrets\", \"version\": \"0.0.1\"}'", 0700},
{"not-a-plugin", "#!/usr/bin/env sh\necho 'not a version'", 0700},
{"unhappy-plugin", "#!/usr/bin/env sh\necho 'sad plugin is sad'; exit 1", 0700},
{"not-executable", "do not execute me", 0400},
}
for _, f := range files {
must.NoError(t, os.WriteFile(filepath.Join(secretsDir, f.name), []byte(f.contents), f.perm))
}
// directories should be ignored
must.NoError(t, os.Mkdir(filepath.Join(secretsDir, "a-directory"), 0700))
// do the fingerprint
resp := &FingerprintResponse{}
err := fp.Fingerprint(req, resp)
must.NoError(t, err)
must.Eq(t, map[string]string{
"plugins.secrets.happy-plugin.version": "0.0.1",
"plugins.secrets.nomad.version": "1.0.0",
"plugins.secrets.vault.version": "1.0.0",
}, resp.Attributes)
// do it again after deleting our one good plugin.
// repeat runs should wipe attributes, so nothing should remain.
node.Attributes = resp.Attributes
must.NoError(t, os.Remove(filepath.Join(secretsDir, "happy-plugin")))
resp = &FingerprintResponse{}
err = fp.Fingerprint(req, resp)
must.NoError(t, err)
must.Eq(t, map[string]string{
"plugins.secrets.happy-plugin.version": "", // empty value means removed
"plugins.secrets.nomad.version": "1.0.0",
"plugins.secrets.vault.version": "1.0.0",
}, resp.Attributes)
} }

View File

@@ -828,6 +828,7 @@ func convertClientConfig(agentConfig *Config) (*clientconfig.Config, error) {
conf.AllocDir = filepath.Join(agentConfig.DataDir, "alloc") conf.AllocDir = filepath.Join(agentConfig.DataDir, "alloc")
conf.HostVolumesDir = filepath.Join(agentConfig.DataDir, "host_volumes") conf.HostVolumesDir = filepath.Join(agentConfig.DataDir, "host_volumes")
conf.HostVolumePluginDir = filepath.Join(agentConfig.DataDir, "host_volume_plugins") conf.HostVolumePluginDir = filepath.Join(agentConfig.DataDir, "host_volume_plugins")
conf.CommonPluginDir = filepath.Join(agentConfig.DataDir, "common_plugins")
dataParent := filepath.Dir(agentConfig.DataDir) dataParent := filepath.Dir(agentConfig.DataDir)
conf.AllocMountsDir = filepath.Join(dataParent, "alloc_mounts") conf.AllocMountsDir = filepath.Join(dataParent, "alloc_mounts")
} }
@@ -846,6 +847,9 @@ func convertClientConfig(agentConfig *Config) (*clientconfig.Config, error) {
if agentConfig.Client.HostVolumesDir != "" { if agentConfig.Client.HostVolumesDir != "" {
conf.HostVolumesDir = agentConfig.Client.HostVolumesDir conf.HostVolumesDir = agentConfig.Client.HostVolumesDir
} }
if agentConfig.Client.CommonPluginDir != "" {
conf.CommonPluginDir = agentConfig.Client.CommonPluginDir
}
if agentConfig.Client.NetworkInterface != "" { if agentConfig.Client.NetworkInterface != "" {
conf.NetworkInterface = agentConfig.Client.NetworkInterface conf.NetworkInterface = agentConfig.Client.NetworkInterface
} }