Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fingerprint: add fingerprinting for CNI plugin versions #15452

Merged
merged 1 commit into from
Dec 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/15452.txt
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
```
2 changes: 1 addition & 1 deletion api/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ require (
github.com/kr/pretty v0.3.1
github.com/mitchellh/go-testing-interface v1.14.1
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
)

Expand Down
4 changes: 2 additions & 2 deletions api/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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/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/shoenig/test v0.4.5 h1:Qb3JfRzzDVTnfQVMeYJm+bsoPlgqVjgawc2lq95ge8Q=
github.com/shoenig/test v0.4.5/go.mod h1:xYtyGBC5Q3kzCNyJg/SjgNpfAa2kvmgA0i5+lQso8x0=
github.com/shoenig/test v0.4.6 h1:S1pAVs5L1TSRen3N1YQNtBZIh9Z6d1PyQSUDUweMTqk=
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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
Expand Down
8 changes: 5 additions & 3 deletions client/fingerprint/cni.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@ import (
"strings"

"github.com/containernetworking/cni/libcni"
log "github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/nomad/nomad/structs"
)

// CNIFingerprint creates a fingerprint of the CNI configuration(s) on the
// Nomad client.
type CNIFingerprint struct {
StaticFingerprinter
logger log.Logger
logger hclog.Logger
}

func NewCNIFingerprint(logger log.Logger) Fingerprint {
func NewCNIFingerprint(logger hclog.Logger) Fingerprint {
return &CNIFingerprint{logger: logger}
}

Expand Down
23 changes: 12 additions & 11 deletions client/fingerprint/fingerprint.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,18 @@ var (
// hostFingerprinters contains the host fingerprints which are available for a
// given platform.
hostFingerprinters = map[string]Factory{
"arch": NewArchFingerprint,
"consul": NewConsulFingerprint,
"cni": NewCNIFingerprint,
"cpu": NewCPUFingerprint,
"host": NewHostFingerprint,
"memory": NewMemoryFingerprint,
"network": NewNetworkFingerprint,
"nomad": NewNomadFingerprint,
"signal": NewSignalFingerprint,
"storage": NewStorageFingerprint,
"vault": NewVaultFingerprint,
"arch": NewArchFingerprint,
"consul": NewConsulFingerprint,
"cni": NewCNIFingerprint, // networks
"cpu": NewCPUFingerprint,
"host": NewHostFingerprint,
"memory": NewMemoryFingerprint,
"network": NewNetworkFingerprint,
"nomad": NewNomadFingerprint,
"plugins_cni": NewPluginsCNIFingerprint,
"signal": NewSignalFingerprint,
"storage": NewStorageFingerprint,
"vault": NewVaultFingerprint,
}

// envFingerprinters contains the fingerprints that are environment specific.
Expand Down
114 changes: 114 additions & 0 deletions client/fingerprint/plugins_cni.go
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 {
lgfa29 marked this conversation as resolved.
Show resolved Hide resolved
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
}
87 changes: 87 additions & 0 deletions client/fingerprint/plugins_cni_test.go
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)
}
3 changes: 3 additions & 0 deletions client/fingerprint/test_fixtures/cni/bridge
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh

echo "CNI bridge plugin v1.0.2"
4 changes: 4 additions & 0 deletions client/fingerprint/test_fixtures/cni/custom
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/sh

echo "Custom v1.2.3 Plugin"

2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ require (
github.com/ryanuber/go-glob v1.0.0
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529
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/stretchr/testify v1.8.1
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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/shirou/gopsutil/v3 v3.22.10 h1:4KMHdfBRYXGF9skjDWiL4RA2N+E8dRdodU/bOZpPoVg=
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.5/go.mod h1:xYtyGBC5Q3kzCNyJg/SjgNpfAa2kvmgA0i5+lQso8x0=
github.com/shoenig/test v0.4.6 h1:S1pAVs5L1TSRen3N1YQNtBZIh9Z6d1PyQSUDUweMTqk=
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/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
Expand Down