-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
dynamic host volumes: fingerprint client plugins (#24589)
- Loading branch information
Showing
13 changed files
with
359 additions
and
71 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: BUSL-1.1 | ||
|
||
package fingerprint | ||
|
||
import ( | ||
"context" | ||
"os" | ||
"strings" | ||
"sync" | ||
"time" | ||
|
||
"github.com/hashicorp/go-hclog" | ||
hvm "github.com/hashicorp/nomad/client/hostvolumemanager" | ||
"github.com/hashicorp/nomad/helper" | ||
) | ||
|
||
func NewPluginsHostVolumeFingerprint(logger hclog.Logger) Fingerprint { | ||
return &DynamicHostVolumePluginFingerprint{ | ||
logger: logger.Named("host_volume_plugins"), | ||
} | ||
} | ||
|
||
var _ ReloadableFingerprint = &DynamicHostVolumePluginFingerprint{} | ||
|
||
type DynamicHostVolumePluginFingerprint struct { | ||
logger hclog.Logger | ||
} | ||
|
||
func (h *DynamicHostVolumePluginFingerprint) Reload() { | ||
// host volume plugins are re-detected on agent reload | ||
} | ||
|
||
func (h *DynamicHostVolumePluginFingerprint) Fingerprint(request *FingerprintRequest, response *FingerprintResponse) error { | ||
// always add "mkdir" plugin | ||
h.logger.Debug("detected plugin built-in", | ||
"plugin_id", hvm.HostVolumePluginMkdirID, "version", hvm.HostVolumePluginMkdirVersion) | ||
defer response.AddAttribute("plugins.host_volume.version."+hvm.HostVolumePluginMkdirID, hvm.HostVolumePluginMkdirVersion) | ||
response.Detected = true | ||
|
||
// this config value will be empty in -dev mode | ||
pluginDir := request.Config.HostVolumePluginDir | ||
if pluginDir == "" { | ||
return nil | ||
} | ||
|
||
plugins, err := GetHostVolumePluginVersions(h.logger, pluginDir) | ||
if err != nil { | ||
if os.IsNotExist(err) { | ||
h.logger.Debug("plugin dir does not exist", "dir", pluginDir) | ||
} else { | ||
h.logger.Warn("error finding plugins", "dir", pluginDir, "error", err) | ||
} | ||
return nil // don't halt agent start | ||
} | ||
|
||
// if this was a reload, wipe what was there before | ||
for k := range request.Node.Attributes { | ||
if strings.HasPrefix(k, "plugins.host_volume.") { | ||
response.RemoveAttribute(k) | ||
} | ||
} | ||
|
||
// set the attribute(s) | ||
for plugin, version := range plugins { | ||
h.logger.Debug("detected plugin", "plugin_id", plugin, "version", version) | ||
response.AddAttribute("plugins.host_volume.version."+plugin, version) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (h *DynamicHostVolumePluginFingerprint) Periodic() (bool, time.Duration) { | ||
return false, 0 | ||
} | ||
|
||
// GetHostVolumePluginVersions finds all the executable files on disk | ||
// that respond to a Version call (arg $1 = 'version' / env $OPERATION = 'version') | ||
// The return map's keys are plugin IDs, and the values are version strings. | ||
func GetHostVolumePluginVersions(log hclog.Logger, pluginDir string) (map[string]string, error) { | ||
files, err := helper.FindExecutableFiles(pluginDir) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
plugins := make(map[string]string) | ||
mut := sync.Mutex{} | ||
var wg sync.WaitGroup | ||
|
||
for file, fullPath := range files { | ||
wg.Add(1) | ||
go func(file, fullPath string) { | ||
defer wg.Done() | ||
// really should take way less than a second | ||
ctx, cancel := context.WithTimeout(context.Background(), time.Second) | ||
defer cancel() | ||
|
||
log := log.With("plugin_id", file) | ||
|
||
p, err := hvm.NewHostVolumePluginExternal(log, file, fullPath, "") | ||
if err != nil { | ||
log.Warn("error getting plugin", "error", err) | ||
return | ||
} | ||
|
||
version, err := p.Version(ctx) | ||
if err != nil { | ||
log.Debug("failed to get version from plugin", "error", err) | ||
return | ||
} | ||
|
||
mut.Lock() | ||
plugins[file] = version.String() | ||
mut.Unlock() | ||
}(file, fullPath) | ||
} | ||
|
||
wg.Wait() | ||
return plugins, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: BUSL-1.1 | ||
|
||
package fingerprint | ||
|
||
import ( | ||
"os" | ||
"path/filepath" | ||
"runtime" | ||
"testing" | ||
|
||
"github.com/hashicorp/nomad/client/config" | ||
hvm "github.com/hashicorp/nomad/client/hostvolumemanager" | ||
"github.com/hashicorp/nomad/helper/testlog" | ||
"github.com/hashicorp/nomad/nomad/structs" | ||
"github.com/shoenig/test/must" | ||
) | ||
|
||
// this is more of a full integration test of: | ||
// fingerprint <- find plugins <- find executables | ||
func TestPluginsHostVolumeFingerprint(t *testing.T) { | ||
cfg := &config.Config{HostVolumePluginDir: ""} | ||
node := &structs.Node{Attributes: map[string]string{}} | ||
req := &FingerprintRequest{Config: cfg, Node: node} | ||
fp := NewPluginsHostVolumeFingerprint(testlog.HCLogger(t)) | ||
|
||
// this fingerprint is not mandatory, so no error should be returned | ||
for name, path := range map[string]string{ | ||
"empty": "", | ||
"non-existent": "/nowhere", | ||
"impossible": "dynamic_host_volumes_test.go", | ||
} { | ||
t.Run(name, func(t *testing.T) { | ||
resp := FingerprintResponse{} | ||
cfg.HostVolumePluginDir = path | ||
err := fp.Fingerprint(req, &resp) | ||
must.NoError(t, err) | ||
must.True(t, resp.Detected) // always true due to "mkdir" built-in | ||
}) | ||
} | ||
|
||
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() | ||
cfg.HostVolumePluginDir = 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 '0.0.1'", 0700}, | ||
{"not-a-plugin", "#!/usr/bin/env sh\necho 'not-a-version'", 0700}, | ||
{"unhappy-plugin", "#!/usr/bin/env sh\necho '0.0.2'; exit 1", 0700}, | ||
{"not-executable", "hello", 0400}, | ||
} | ||
for _, f := range files { | ||
must.NoError(t, os.WriteFile(filepath.Join(tmp, f.name), []byte(f.contents), f.perm)) | ||
} | ||
// directories should be ignored | ||
must.NoError(t, os.Mkdir(filepath.Join(tmp, "a-directory"), 0700)) | ||
|
||
// do the fingerprint | ||
resp := FingerprintResponse{} | ||
err := fp.Fingerprint(req, &resp) | ||
must.NoError(t, err) | ||
must.Eq(t, map[string]string{ | ||
"plugins.host_volume.version.happy-plugin": "0.0.1", | ||
"plugins.host_volume.version.mkdir": hvm.HostVolumePluginMkdirVersion, // built-in | ||
}, 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(tmp, "happy-plugin"))) | ||
|
||
resp = FingerprintResponse{} | ||
err = fp.Fingerprint(req, &resp) | ||
must.NoError(t, err) | ||
must.Eq(t, map[string]string{ | ||
"plugins.host_volume.version.happy-plugin": "", // empty value means removed | ||
|
||
"plugins.host_volume.version.mkdir": hvm.HostVolumePluginMkdirVersion, // built-in | ||
}, resp.Attributes) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.