github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/client/fingerprint/plugins_cni.go (about)

     1  package fingerprint
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"os/exec"
     8  	"path/filepath"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/hashicorp/go-hclog"
    13  	"github.com/hashicorp/go-version"
    14  )
    15  
    16  const (
    17  	cniPluginAttribute = "plugins.cni.version"
    18  )
    19  
    20  // PluginsCNIFingerprint creates a fingerprint of the CNI plugins present on the
    21  // CNI plugin path specified for the Nomad client.
    22  type PluginsCNIFingerprint struct {
    23  	StaticFingerprinter
    24  	logger hclog.Logger
    25  	lister func(string) ([]os.DirEntry, error)
    26  }
    27  
    28  func NewPluginsCNIFingerprint(logger hclog.Logger) Fingerprint {
    29  	return &PluginsCNIFingerprint{
    30  		logger: logger.Named("cni_plugins"),
    31  		lister: os.ReadDir,
    32  	}
    33  }
    34  
    35  func (f *PluginsCNIFingerprint) Fingerprint(req *FingerprintRequest, resp *FingerprintResponse) error {
    36  	cniPath := req.Config.CNIPath
    37  	if cniPath == "" {
    38  		// this will be set to default by client; if empty then lets just do
    39  		// nothing rather than re-assume a default of our own
    40  		return nil
    41  	}
    42  
    43  	// list the cni_path directory
    44  	entries, err := f.lister(cniPath)
    45  	switch {
    46  	case err != nil:
    47  		f.logger.Warn("failed to read CNI plugins directory", "cni_path", cniPath, "error", err)
    48  		resp.Detected = false
    49  		return nil
    50  	case len(entries) == 0:
    51  		f.logger.Debug("no CNI plugins found", "cni_path", cniPath)
    52  		resp.Detected = true
    53  		return nil
    54  	}
    55  
    56  	// for each file in cni_path, detect executables and try to get their version
    57  	for _, entry := range entries {
    58  		v, ok := f.detectOne(cniPath, entry)
    59  		if ok {
    60  			resp.AddAttribute(f.attribute(entry.Name()), v)
    61  		}
    62  	}
    63  
    64  	// detection complete, regardless of results
    65  	resp.Detected = true
    66  	return nil
    67  }
    68  
    69  func (f *PluginsCNIFingerprint) attribute(filename string) string {
    70  	return fmt.Sprintf("%s.%s", cniPluginAttribute, filename)
    71  }
    72  
    73  func (f *PluginsCNIFingerprint) detectOne(cniPath string, entry os.DirEntry) (string, bool) {
    74  	fi, err := entry.Info()
    75  	if err != nil {
    76  		f.logger.Debug("failed to read cni directory entry", "error", err)
    77  		return "", false
    78  	}
    79  
    80  	if fi.Mode()&0o111 == 0 {
    81  		f.logger.Debug("unexpected non-executable in cni plugin directory", "name", fi.Name())
    82  		return "", false // not executable
    83  	}
    84  
    85  	exePath := filepath.Join(cniPath, fi.Name())
    86  	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    87  	defer cancel()
    88  
    89  	// best effort attempt to get a version from the executable, otherwise
    90  	// the version will be "unknown"
    91  	// execute with no args; at least container-networking plugins respond with
    92  	// version string in this case, which makes Windows support simpler
    93  	cmd := exec.CommandContext(ctx, exePath)
    94  	output, err := cmd.CombinedOutput()
    95  	if err != nil {
    96  		f.logger.Debug("failed to detect CNI plugin version", "name", fi.Name(), "error", err)
    97  		return "unknown", false
    98  	}
    99  
   100  	// try to find semantic versioning string
   101  	// e.g.
   102  	//  /opt/cni/bin/bridge <no args>
   103  	//  CNI bridge plugin v1.0.0
   104  	tokens := strings.Fields(string(output))
   105  	for i := len(tokens) - 1; i >= 0; i-- {
   106  		token := tokens[i]
   107  		if _, parseErr := version.NewSemver(token); parseErr == nil {
   108  			return token, true
   109  		}
   110  	}
   111  
   112  	f.logger.Debug("failed to parse CNI plugin version", "name", fi.Name())
   113  	return "unknown", false
   114  }