-
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.
fingerprint: add fingerprinting for CNI plugins presense and version (#…
…15452) This PR adds a fingerprinter to set the attribute "plugins.cni.version.<name>" => "<version>" for each CNI plugin in <client>.cni_path (/opt/cni/bin by default).
- Loading branch information
Showing
11 changed files
with
234 additions
and
20 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
```release-note:improvement | ||
fingerprint: Detect CNI plugins and set versions as node 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
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,114 @@ | ||
package fingerprint | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"os" | ||
"os/exec" | ||
"path/filepath" | ||
"strings" | ||
"time" | ||
|
||
"github.com/hashicorp/go-hclog" | ||
"github.com/hashicorp/go-version" | ||
) | ||
|
||
const ( | ||
cniPluginAttribute = "plugins.cni.version" | ||
) | ||
|
||
// PluginsCNIFingerprint creates a fingerprint of the CNI plugins present on the | ||
// CNI plugin path specified for the Nomad client. | ||
type PluginsCNIFingerprint struct { | ||
StaticFingerprinter | ||
logger hclog.Logger | ||
lister func(string) ([]os.DirEntry, error) | ||
} | ||
|
||
func NewPluginsCNIFingerprint(logger hclog.Logger) Fingerprint { | ||
return &PluginsCNIFingerprint{ | ||
logger: logger.Named("cni_plugins"), | ||
lister: os.ReadDir, | ||
} | ||
} | ||
|
||
func (f *PluginsCNIFingerprint) Fingerprint(req *FingerprintRequest, resp *FingerprintResponse) error { | ||
cniPath := req.Config.CNIPath | ||
if cniPath == "" { | ||
// this will be set to default by client; if empty then lets just do | ||
// nothing rather than re-assume a default of our own | ||
return nil | ||
} | ||
|
||
// list the cni_path directory | ||
entries, err := f.lister(cniPath) | ||
switch { | ||
case err != nil: | ||
f.logger.Warn("failed to read CNI plugins directory", "cni_path", cniPath, "error", err) | ||
resp.Detected = false | ||
return nil | ||
case len(entries) == 0: | ||
f.logger.Debug("no CNI plugins found", "cni_path", cniPath) | ||
resp.Detected = true | ||
return nil | ||
} | ||
|
||
// for each file in cni_path, detect executables and try to get their version | ||
for _, entry := range entries { | ||
v, ok := f.detectOne(cniPath, entry) | ||
if ok { | ||
resp.AddAttribute(f.attribute(entry.Name()), v) | ||
} | ||
} | ||
|
||
// detection complete, regardless of results | ||
resp.Detected = true | ||
return nil | ||
} | ||
|
||
func (f *PluginsCNIFingerprint) attribute(filename string) string { | ||
return fmt.Sprintf("%s.%s", cniPluginAttribute, filename) | ||
} | ||
|
||
func (f *PluginsCNIFingerprint) detectOne(cniPath string, entry os.DirEntry) (string, bool) { | ||
fi, err := entry.Info() | ||
if err != nil { | ||
f.logger.Debug("failed to read cni directory entry", "error", err) | ||
return "", false | ||
} | ||
|
||
if fi.Mode()&0o111 == 0 { | ||
f.logger.Debug("unexpected non-executable in cni plugin directory", "name", fi.Name()) | ||
return "", false // not executable | ||
} | ||
|
||
exePath := filepath.Join(cniPath, fi.Name()) | ||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) | ||
defer cancel() | ||
|
||
// best effort attempt to get a version from the executable, otherwise | ||
// the version will be "unknown" | ||
// execute with no args; at least container-networking plugins respond with | ||
// version string in this case, which makes Windows support simpler | ||
cmd := exec.CommandContext(ctx, exePath) | ||
output, err := cmd.CombinedOutput() | ||
if err != nil { | ||
f.logger.Debug("failed to detect CNI plugin version", "name", fi.Name(), "error", err) | ||
return "unknown", false | ||
} | ||
|
||
// try to find semantic versioning string | ||
// e.g. | ||
// /opt/cni/bin/bridge <no args> | ||
// CNI bridge plugin v1.0.0 | ||
tokens := strings.Fields(string(output)) | ||
for i := len(tokens) - 1; i >= 0; i-- { | ||
token := tokens[i] | ||
if _, parseErr := version.NewSemver(token); parseErr == nil { | ||
return token, true | ||
} | ||
} | ||
|
||
f.logger.Debug("failed to parse CNI plugin version", "name", fi.Name()) | ||
return "unknown", false | ||
} |
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,87 @@ | ||
package fingerprint | ||
|
||
import ( | ||
"os" | ||
"testing" | ||
|
||
"github.com/hashicorp/nomad/ci" | ||
"github.com/hashicorp/nomad/client/config" | ||
"github.com/hashicorp/nomad/helper/testlog" | ||
"github.com/shoenig/test/must" | ||
) | ||
|
||
func TestPluginsCNIFingerprint_Fingerprint_present(t *testing.T) { | ||
ci.Parallel(t) | ||
|
||
f := NewPluginsCNIFingerprint(testlog.HCLogger(t)) | ||
request := &FingerprintRequest{ | ||
Config: &config.Config{ | ||
CNIPath: "./test_fixtures/cni", | ||
}, | ||
} | ||
response := new(FingerprintResponse) | ||
|
||
err := f.Fingerprint(request, response) | ||
must.NoError(t, err) | ||
must.True(t, response.Detected) | ||
attrCustom := f.(*PluginsCNIFingerprint).attribute("custom") | ||
attrBridge := f.(*PluginsCNIFingerprint).attribute("bridge") | ||
must.Eq(t, "v1.2.3", response.Attributes[attrCustom]) | ||
must.Eq(t, "v1.0.2", response.Attributes[attrBridge]) | ||
} | ||
|
||
func TestPluginsCNIFingerprint_Fingerprint_absent(t *testing.T) { | ||
ci.Parallel(t) | ||
|
||
f := NewPluginsCNIFingerprint(testlog.HCLogger(t)) | ||
request := &FingerprintRequest{ | ||
Config: &config.Config{ | ||
CNIPath: "/does/not/exist", | ||
}, | ||
} | ||
response := new(FingerprintResponse) | ||
|
||
err := f.Fingerprint(request, response) | ||
must.NoError(t, err) | ||
must.False(t, response.Detected) | ||
attrCustom := f.(*PluginsCNIFingerprint).attribute("custom") | ||
attrBridge := f.(*PluginsCNIFingerprint).attribute("bridge") | ||
must.MapNotContainsKeys(t, response.Attributes, []string{attrCustom, attrBridge}) | ||
} | ||
|
||
func TestPluginsCNIFingerprint_Fingerprint_empty(t *testing.T) { | ||
ci.Parallel(t) | ||
|
||
lister := func(string) ([]os.DirEntry, error) { | ||
// return an empty slice of directory entries | ||
// i.e. no plugins present | ||
return nil, nil | ||
} | ||
|
||
f := NewPluginsCNIFingerprint(testlog.HCLogger(t)) | ||
f.(*PluginsCNIFingerprint).lister = lister | ||
request := &FingerprintRequest{ | ||
Config: &config.Config{ | ||
CNIPath: "./test_fixtures/cni", | ||
}, | ||
} | ||
response := new(FingerprintResponse) | ||
|
||
err := f.Fingerprint(request, response) | ||
must.NoError(t, err) | ||
must.True(t, response.Detected) | ||
} | ||
|
||
func TestPluginsCNIFingerprint_Fingerprint_unset(t *testing.T) { | ||
ci.Parallel(t) | ||
|
||
f := NewPluginsCNIFingerprint(testlog.HCLogger(t)) | ||
request := &FingerprintRequest{ | ||
Config: new(config.Config), | ||
} | ||
response := new(FingerprintResponse) | ||
|
||
err := f.Fingerprint(request, response) | ||
must.NoError(t, err) | ||
must.False(t, response.Detected) | ||
} |
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,3 @@ | ||
#!/bin/sh | ||
|
||
echo "CNI bridge plugin v1.0.2" |
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,4 @@ | ||
#!/bin/sh | ||
|
||
echo "Custom v1.2.3 Plugin" | ||
|
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