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).
This commit is contained in:
parent
541ca94576
commit
3ed37b0b1d
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:improvement
|
||||||
|
fingerprint: Detect CNI plugins and set versions as node attributes
|
||||||
|
```
|
|
@ -11,7 +11,7 @@ require (
|
||||||
github.com/kr/pretty v0.3.1
|
github.com/kr/pretty v0.3.1
|
||||||
github.com/mitchellh/go-testing-interface v1.14.1
|
github.com/mitchellh/go-testing-interface v1.14.1
|
||||||
github.com/mitchellh/mapstructure v1.5.0
|
github.com/mitchellh/mapstructure v1.5.0
|
||||||
github.com/shoenig/test v0.4.5
|
github.com/shoenig/test v0.4.6
|
||||||
github.com/stretchr/testify v1.8.1
|
github.com/stretchr/testify v1.8.1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -29,8 +29,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/shoenig/test v0.4.5 h1:Qb3JfRzzDVTnfQVMeYJm+bsoPlgqVjgawc2lq95ge8Q=
|
github.com/shoenig/test v0.4.6 h1:S1pAVs5L1TSRen3N1YQNtBZIh9Z6d1PyQSUDUweMTqk=
|
||||||
github.com/shoenig/test v0.4.5/go.mod h1:xYtyGBC5Q3kzCNyJg/SjgNpfAa2kvmgA0i5+lQso8x0=
|
github.com/shoenig/test v0.4.6/go.mod h1:xYtyGBC5Q3kzCNyJg/SjgNpfAa2kvmgA0i5+lQso8x0=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
|
|
@ -6,16 +6,18 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/containernetworking/cni/libcni"
|
"github.com/containernetworking/cni/libcni"
|
||||||
log "github.com/hashicorp/go-hclog"
|
"github.com/hashicorp/go-hclog"
|
||||||
"github.com/hashicorp/nomad/nomad/structs"
|
"github.com/hashicorp/nomad/nomad/structs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CNIFingerprint creates a fingerprint of the CNI configuration(s) on the
|
||||||
|
// Nomad client.
|
||||||
type CNIFingerprint struct {
|
type CNIFingerprint struct {
|
||||||
StaticFingerprinter
|
StaticFingerprinter
|
||||||
logger log.Logger
|
logger hclog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCNIFingerprint(logger log.Logger) Fingerprint {
|
func NewCNIFingerprint(logger hclog.Logger) Fingerprint {
|
||||||
return &CNIFingerprint{logger: logger}
|
return &CNIFingerprint{logger: logger}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,17 +29,18 @@ var (
|
||||||
// hostFingerprinters contains the host fingerprints which are available for a
|
// hostFingerprinters contains the host fingerprints which are available for a
|
||||||
// given platform.
|
// given platform.
|
||||||
hostFingerprinters = map[string]Factory{
|
hostFingerprinters = map[string]Factory{
|
||||||
"arch": NewArchFingerprint,
|
"arch": NewArchFingerprint,
|
||||||
"consul": NewConsulFingerprint,
|
"consul": NewConsulFingerprint,
|
||||||
"cni": NewCNIFingerprint,
|
"cni": NewCNIFingerprint, // networks
|
||||||
"cpu": NewCPUFingerprint,
|
"cpu": NewCPUFingerprint,
|
||||||
"host": NewHostFingerprint,
|
"host": NewHostFingerprint,
|
||||||
"memory": NewMemoryFingerprint,
|
"memory": NewMemoryFingerprint,
|
||||||
"network": NewNetworkFingerprint,
|
"network": NewNetworkFingerprint,
|
||||||
"nomad": NewNomadFingerprint,
|
"nomad": NewNomadFingerprint,
|
||||||
"signal": NewSignalFingerprint,
|
"plugins_cni": NewPluginsCNIFingerprint,
|
||||||
"storage": NewStorageFingerprint,
|
"signal": NewSignalFingerprint,
|
||||||
"vault": NewVaultFingerprint,
|
"storage": NewStorageFingerprint,
|
||||||
|
"vault": NewVaultFingerprint,
|
||||||
}
|
}
|
||||||
|
|
||||||
// envFingerprinters contains the fingerprints that are environment specific.
|
// envFingerprinters contains the fingerprints that are environment specific.
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
echo "CNI bridge plugin v1.0.2"
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
echo "Custom v1.2.3 Plugin"
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -110,7 +110,7 @@ require (
|
||||||
github.com/ryanuber/go-glob v1.0.0
|
github.com/ryanuber/go-glob v1.0.0
|
||||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529
|
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529
|
||||||
github.com/shirou/gopsutil/v3 v3.22.10
|
github.com/shirou/gopsutil/v3 v3.22.10
|
||||||
github.com/shoenig/test v0.4.5
|
github.com/shoenig/test v0.4.6
|
||||||
github.com/skratchdot/open-golang v0.0.0-20160302144031-75fb7ed4208c
|
github.com/skratchdot/open-golang v0.0.0-20160302144031-75fb7ed4208c
|
||||||
github.com/stretchr/testify v1.8.1
|
github.com/stretchr/testify v1.8.1
|
||||||
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635
|
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -1171,8 +1171,8 @@ github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||||
github.com/shirou/gopsutil/v3 v3.22.10 h1:4KMHdfBRYXGF9skjDWiL4RA2N+E8dRdodU/bOZpPoVg=
|
github.com/shirou/gopsutil/v3 v3.22.10 h1:4KMHdfBRYXGF9skjDWiL4RA2N+E8dRdodU/bOZpPoVg=
|
||||||
github.com/shirou/gopsutil/v3 v3.22.10/go.mod h1:QNza6r4YQoydyCfo6rH0blGfKahgibh4dQmV5xdFkQk=
|
github.com/shirou/gopsutil/v3 v3.22.10/go.mod h1:QNza6r4YQoydyCfo6rH0blGfKahgibh4dQmV5xdFkQk=
|
||||||
github.com/shoenig/test v0.4.5 h1:Qb3JfRzzDVTnfQVMeYJm+bsoPlgqVjgawc2lq95ge8Q=
|
github.com/shoenig/test v0.4.6 h1:S1pAVs5L1TSRen3N1YQNtBZIh9Z6d1PyQSUDUweMTqk=
|
||||||
github.com/shoenig/test v0.4.5/go.mod h1:xYtyGBC5Q3kzCNyJg/SjgNpfAa2kvmgA0i5+lQso8x0=
|
github.com/shoenig/test v0.4.6/go.mod h1:xYtyGBC5Q3kzCNyJg/SjgNpfAa2kvmgA0i5+lQso8x0=
|
||||||
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
|
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
|
||||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
|
|
Loading…
Reference in New Issue