diff --git a/README.md b/README.md index 49c2b8b..e0ecac0 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,8 @@ Usage of generic-device-plugin: A "count" can be specified to allow a discovered device group to be scheduled multiple times. For example, to permit allocation of the FUSE device 10 times: {"name": "fuse", "groups": [{"count": 10, "paths": [{"path": "/dev/fuse"}]}]} Note: if omitted, "count" is assumed to be 1 + An "optional" field can be specified for individual paths to allow containers to start even when some devices are missing. + For example, to expose serial devices that may or may not be present: {"name": "serial", "groups": [{"paths": [{"path": "/dev/ttyS0", "optional": true}, {"path": "/dev/ttyUSB0", "optional": true}]}]} If mountPath is a directory, the device will be mounted to the directory with the name of the device. For example, to expose the serial devices to the /dev/serial directory: {"name": "serial", "groups": [{"paths": [{"path": "/dev/ttyUSB*", "mountPath": "/dev/serial/"}]}]} --domain string The domain to use when when declaring devices. (default "squat.ai") diff --git a/config.go b/config.go index 4903c06..0da619a 100644 --- a/config.go +++ b/config.go @@ -47,6 +47,8 @@ For example, to expose a CH340 serial converter: {"name": "ch340", "groups": [{" A "count" can be specified to allow a discovered device group to be scheduled multiple times. For example, to permit allocation of the FUSE device 10 times: {"name": "fuse", "groups": [{"count": 10, "paths": [{"path": "/dev/fuse"}]}]} Note: if omitted, "count" is assumed to be 1 +An "optional" field can be specified for individual paths to allow containers to start even when some devices are missing. +For example, to expose serial devices that may or may not be present: {"name": "serial", "groups": [{"paths": [{"path": "/dev/ttyS0", "optional": true}, {"path": "/dev/ttyUSB0", "optional": true}]}]} If mountPath is a directory, the device will be mounted to the directory with the name of the device. For example, to expose the serial devices to the /dev/serial directory: {"name": "serial", "groups": [{"paths": [{"path": "/dev/ttyUSB*", "mountPath": "/dev/serial/"}]}]}`) flag.String("plugin-directory", v1beta1.DevicePluginPath, "The directory in which to create plugin sockets.") diff --git a/deviceplugin/path.go b/deviceplugin/path.go index 1124435..d9f5943 100644 --- a/deviceplugin/path.go +++ b/deviceplugin/path.go @@ -18,6 +18,7 @@ import ( "crypto/sha1" "fmt" "io/fs" + "math" "path/filepath" "sort" "strconv" @@ -53,6 +54,11 @@ type Path struct { // then the group will provide 5 pairs of devices. // When unspecified, Limit defaults to 1. Limit uint `json:"limit,omitempty"` + // Optional specifies whether this device path is optional. + // When true, if the device path does not exist, it will be ignored instead of causing an error. + // This allows containers to start even when some devices are not present on the system. + // When unspecified, Optional defaults to false. + Optional bool `json:"optional,omitempty"` } // PathType represents the kinds of file-system nodes that can be scheduled. @@ -71,19 +77,26 @@ func (gp *GenericPlugin) discoverPath() ([]device, error) { for _, group := range gp.ds.Groups { paths := make([][]string, len(group.Paths)) var length int - var limitLength int + limitLength := math.MaxInt + // Track which paths have matches (used for optional paths). + pathHasMatches := make([]bool, len(group.Paths)) // Discover all the devices matching each pattern in the Paths group. for i, path := range group.Paths { matches, err := fs.Glob(gp.fs, path.Path) if err != nil { return nil, err } + // If no matches found and path is optional, skip it. + if len(matches) == 0 && path.Optional { + continue + } + pathHasMatches[i] = true sort.Strings(matches) for j := uint(0); j < path.Limit; j++ { paths[i] = append(paths[i], matches...) } // Keep track of the shortest reusable length in the group. - if i == 0 || len(paths[i]) < limitLength { + if len(paths[i]) < limitLength { limitLength = len(paths[i]) } // Keep track of the greatest natural length in the group. @@ -105,6 +118,10 @@ func (gp *GenericPlugin) discoverPath() ([]device, error) { }, } for k, path := range group.Paths { + // Skip paths that had no matches (optional and missing). + if !pathHasMatches[k] { + continue + } mountPath = path.MountPath if mountPath == "" { mountPath = paths[k][i] diff --git a/deviceplugin/path_test.go b/deviceplugin/path_test.go index c9f10fd..af41e35 100644 --- a/deviceplugin/path_test.go +++ b/deviceplugin/path_test.go @@ -200,6 +200,115 @@ func TestDiscoverPaths(t *testing.T) { }, err: nil, }, + { + name: "optional paths - some missing", + ds: &DeviceSpec{ + Name: "serial", + Groups: []*Group{ + { + Paths: []*Path{ + { + Path: "/dev/ttyS0", + Optional: true, + }, + { + Path: "/dev/ttyUSB0", + Optional: true, + }, + { + Path: "/dev/ttyUSB1", + Optional: true, + }, + }, + }, + }, + }, + fs: fstest.MapFS{ + "dev/ttyUSB0": {}, + }, + out: []device{ + { + deviceSpecs: []*v1beta1.DeviceSpec{ + { + ContainerPath: "/dev/ttyUSB0", + HostPath: "/dev/ttyUSB0", + }, + }, + }, + }, + err: nil, + }, + { + name: "optional paths - all present", + ds: &DeviceSpec{ + Name: "serial", + Groups: []*Group{ + { + Paths: []*Path{ + { + Path: "/dev/ttyS0", + Optional: true, + }, + { + Path: "/dev/ttyUSB0", + Optional: true, + }, + { + Path: "/dev/ttyUSB1", + Optional: true, + }, + }, + }, + }, + }, + fs: fstest.MapFS{ + "dev/ttyS0": {}, + "dev/ttyUSB0": {}, + "dev/ttyUSB1": {}, + }, + out: []device{ + { + deviceSpecs: []*v1beta1.DeviceSpec{ + { + ContainerPath: "/dev/ttyS0", + HostPath: "/dev/ttyS0", + }, + { + ContainerPath: "/dev/ttyUSB0", + HostPath: "/dev/ttyUSB0", + }, + { + ContainerPath: "/dev/ttyUSB1", + HostPath: "/dev/ttyUSB1", + }, + }, + }, + }, + err: nil, + }, + { + name: "optional paths - all missing", + ds: &DeviceSpec{ + Name: "serial", + Groups: []*Group{ + { + Paths: []*Path{ + { + Path: "/dev/ttyS0", + Optional: true, + }, + { + Path: "/dev/ttyUSB0", + Optional: true, + }, + }, + }, + }, + }, + fs: fstest.MapFS{}, + out: []device{}, + err: nil, + }, } { t.Run(tc.name, func(t *testing.T) { tc.ds.Default()