From e20c7c38b626278a6d8b6687340a81eb7963f1a5 Mon Sep 17 00:00:00 2001 From: Seth Hoenig Date: Thu, 1 Dec 2022 14:17:05 -0600 Subject: [PATCH] fingerprint: add fingerprinting for CNI plugins presense and version This PR adds a fingerprinter to set the attributes "cni.plugins" -> "true" or "false" if contains CNI plugins "cni.plugins.version" -> version of the CNI plugins if present --- .changelog/15452.txt | 3 + client/fingerprint/cni.go | 8 ++- client/fingerprint/fingerprint.go | 23 +++---- client/fingerprint/plugins_cni.go | 67 +++++++++++++++++++++ client/fingerprint/plugins_cni_test.go | 67 +++++++++++++++++++++ client/fingerprint/test_fixtures/cni/bridge | 3 + 6 files changed, 157 insertions(+), 14 deletions(-) create mode 100644 .changelog/15452.txt create mode 100644 client/fingerprint/plugins_cni.go create mode 100644 client/fingerprint/plugins_cni_test.go create mode 100755 client/fingerprint/test_fixtures/cni/bridge diff --git a/.changelog/15452.txt b/.changelog/15452.txt new file mode 100644 index 000000000000..74ab9ed8d0c5 --- /dev/null +++ b/.changelog/15452.txt @@ -0,0 +1,3 @@ +```release-note:improvement +fingerprint: Add fingerprinting for CNI containernetworking plugins +``` diff --git a/client/fingerprint/cni.go b/client/fingerprint/cni.go index b4bfff69597f..351d7b3d2402 100644 --- a/client/fingerprint/cni.go +++ b/client/fingerprint/cni.go @@ -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} } diff --git a/client/fingerprint/fingerprint.go b/client/fingerprint/fingerprint.go index a12ea98f442a..39c8dcba964b 100644 --- a/client/fingerprint/fingerprint.go +++ b/client/fingerprint/fingerprint.go @@ -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. diff --git a/client/fingerprint/plugins_cni.go b/client/fingerprint/plugins_cni.go new file mode 100644 index 000000000000..02d6c24f5cf6 --- /dev/null +++ b/client/fingerprint/plugins_cni.go @@ -0,0 +1,67 @@ +package fingerprint + +import ( + "context" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/hashicorp/go-hclog" +) + +const ( + defaultCNIPath = "/opt/cni/bin" + 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 == "" { + cniPath = defaultCNIPath + } + + entries, err := f.lister(cniPath) + switch { + case err != nil: + f.logger.Debug("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 + } + + bridgePath := filepath.Join(cniPath, "bridge") + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + cmd := exec.CommandContext(ctx, bridgePath, "--version") + output, err := cmd.CombinedOutput() + if err != nil { + f.logger.Debug("failed to detect CNI plugins version", "error", err) + return nil + } + tokens := strings.Fields(string(output)) + version := tokens[len(tokens)-1] + f.logger.Debug("detected CNI plugins", "version", version) + resp.AddAttribute(cniPluginAttribute, version) + resp.Detected = true + return nil +} diff --git a/client/fingerprint/plugins_cni_test.go b/client/fingerprint/plugins_cni_test.go new file mode 100644 index 000000000000..193643c98fe3 --- /dev/null +++ b/client/fingerprint/plugins_cni_test.go @@ -0,0 +1,67 @@ +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) + must.StrHasPrefix(t, "v1.0.2", response.Attributes[cniPluginAttribute]) +} + +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) + _, exists := response.Attributes[cniPluginAttribute] + must.False(t, exists) +} + +func TestPluginsCNIFingerprint_Fingerprint_empty(t *testing.T) { + ci.Parallel(t) + + lister := func(string) ([]os.DirEntry, error) { + return nil, nil + } + + f := NewPluginsCNIFingerprint(testlog.HCLogger(t)) + f.(*PluginsCNIFingerprint).lister = lister + request := &FingerprintRequest{ + Config: new(config.Config), + } + response := new(FingerprintResponse) + + err := f.Fingerprint(request, response) + must.NoError(t, err) + must.True(t, response.Detected) + _, exists := response.Attributes[cniPluginAttribute] + must.False(t, exists) +} diff --git a/client/fingerprint/test_fixtures/cni/bridge b/client/fingerprint/test_fixtures/cni/bridge new file mode 100755 index 000000000000..0b7f14f7f506 --- /dev/null +++ b/client/fingerprint/test_fixtures/cni/bridge @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "CNI bridge plugin v1.0.2"