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 }