1
0
Fork 0
mirror of https://github.com/kubernetes-sigs/node-feature-discovery.git synced 2024-12-14 11:57:51 +00:00

feat: support raw features

Signed-off-by: AhmedGrati <ahmedgrati1999@gmail.com>
This commit is contained in:
AhmedGrati 2023-09-30 21:45:31 +01:00
parent 076ed3c057
commit 3130898d58
4 changed files with 153 additions and 39 deletions

View file

@ -355,6 +355,41 @@ included in the list of accepted features.
> tag is only evaluated in each re-discovery period, and the expiration of > tag is only evaluated in each re-discovery period, and the expiration of
> node labels is not tracked. > node labels is not tracked.
To exclude specific features from the `local.feature` Feature, you can use the
`# +no-feature` directive. The `# +no-label` directive causes the feature to
be excluded from the `local.label` Feature and a node label not to be generated.
Considering the following file:
```plaintext
# +no-feature
label-only=value
my-feature=value
foo=bar
# +no-label
foo=baz
```
Processing the above file would result in the following Features:
```yaml
local.features:
foo: baz
my-feature: value
local.labels:
label-only: value
my-feature: value
```
and the following labels added to the Node:
```plaintext
feature.node.kubernetes.io/label-only=value
feature.node.kubernetes.io/my-feature=value
```
### Mounts ### Mounts
The standard NFD deployments contain `hostPath` mounts for The standard NFD deployments contain `hostPath` mounts for
@ -775,7 +810,8 @@ The following features are available for matching:
| | | **`major`** | int | First component of the kernel version (e.g. 4') | | | **`major`** | int | First component of the kernel version (e.g. 4')
| | | **`minor`** | int | Second component of the kernel version (e.g. 5') | | | **`minor`** | int | Second component of the kernel version (e.g. 5')
| | | **`revision`** | int | Third component of the kernel version (e.g. 6') | | | **`revision`** | int | Third component of the kernel version (e.g. 6')
| **`local.label`** | attribute | | | Features feature files and hooks, i.e. labels from the [*local* feature source](#local-feature-source) | **`local.label`** | attribute | | | Labels from feature files and hooks, i.e. labels from the [*local* feature source](#local-feature-source)
| **`local.feature`** | attribute | | | Features from feature files and hooks, i.e. features from the [*local* feature source](#local-feature-source)
| | | **`<label-name>`** | string | Label `<label-name>` created by the local feature source, value equals the value of the label | | | **`<label-name>`** | string | Label `<label-name>` created by the local feature source, value equals the value of the label
| **`memory.nv`** | instance | | | NVDIMM devices present in the system | **`memory.nv`** | instance | | | NVDIMM devices present in the system
| | | **`<sysfs-attribute>`** | string | Value of the sysfs device attribute, available attributes: `devtype`, `mode` | | | **`<sysfs-attribute>`** | string | Value of the sysfs device attribute, available attributes: `devtype`, `mode`

View file

@ -38,9 +38,22 @@ const Name = "local"
// LabelFeature of this feature source // LabelFeature of this feature source
const LabelFeature = "label" const LabelFeature = "label"
// ExpiryTimeKey is the key of this feature source indicating // RawFeature of this feature source
// when features should be removed. const RawFeature = "feature"
const ExpiryTimeKey = "expiry-time"
const (
// ExpiryTimeKey is the key of this feature source indicating
// when features should be removed.
DirectiveExpiryTime = "expiry-time"
// NoLabel indicates whether the feature should be included
// in exposed labels or not.
DirectiveNoLabel = "no-label"
// NoFeature indicates whether the feature should be included
// in exposed raw features or not.
DirectiveNoFeature = "no-feature"
)
// DirectivePrefix defines the prefix of directives that should be parsed // DirectivePrefix defines the prefix of directives that should be parsed
const DirectivePrefix = "# +" const DirectivePrefix = "# +"
@ -66,7 +79,9 @@ type Config struct {
// parsingOpts contains options used for directives parsing // parsingOpts contains options used for directives parsing
type parsingOpts struct { type parsingOpts struct {
ExpiryTime time.Time ExpiryTime time.Time
SkipLabel bool
SkipFeature bool
} }
// Singleton source instance // Singleton source instance
@ -121,7 +136,7 @@ func newDefaultConfig() *Config {
func (s *localSource) Discover() error { func (s *localSource) Discover() error {
s.features = nfdv1alpha1.NewFeatures() s.features = nfdv1alpha1.NewFeatures()
featuresFromFiles, err := getFeaturesFromFiles() featuresFromFiles, labelsFromFiles, err := getFeaturesFromFiles()
if err != nil { if err != nil {
klog.ErrorS(err, "failed to read feature files") klog.ErrorS(err, "failed to read feature files")
} }
@ -131,7 +146,7 @@ func (s *localSource) Discover() error {
klog.InfoS("starting hooks...") klog.InfoS("starting hooks...")
klog.InfoS("NOTE: hooks are deprecated and will be completely removed in a future release.") klog.InfoS("NOTE: hooks are deprecated and will be completely removed in a future release.")
featuresFromHooks, err := getFeaturesFromHooks() featuresFromHooks, labelsFromHooks, err := getFeaturesFromHooks()
if err != nil { if err != nil {
klog.ErrorS(err, "failed to run hooks") klog.ErrorS(err, "failed to run hooks")
} }
@ -139,13 +154,22 @@ func (s *localSource) Discover() error {
// Merge features from hooks and files // Merge features from hooks and files
for k, v := range featuresFromHooks { for k, v := range featuresFromHooks {
if old, ok := featuresFromFiles[k]; ok { if old, ok := featuresFromFiles[k]; ok {
klog.InfoS("overriding label value", "labelKey", k, "oldValue", old, "newValue", v) klog.InfoS("overriding feature value", "featureKey", k, "oldValue", old, "newValue", v)
} }
featuresFromFiles[k] = v featuresFromFiles[k] = v
} }
// Merge labels from hooks and files
for k, v := range labelsFromHooks {
if old, ok := labelsFromFiles[k]; ok {
klog.InfoS("overriding label value", "labelKey", k, "oldValue", old, "newValue", v)
}
labelsFromHooks[k] = v
}
} }
s.features.Attributes[LabelFeature] = nfdv1alpha1.NewAttributeFeatures(featuresFromFiles) s.features.Attributes[LabelFeature] = nfdv1alpha1.NewAttributeFeatures(labelsFromFiles)
s.features.Attributes[RawFeature] = nfdv1alpha1.NewAttributeFeatures(featuresFromFiles)
klog.V(3).InfoS("discovered features", "featureSource", s.Name(), "features", utils.DelayedDumper(s.features)) klog.V(3).InfoS("discovered features", "featureSource", s.Name(), "features", utils.DelayedDumper(s.features))
@ -169,18 +193,21 @@ func parseDirectives(line string, opts *parsingOpts) error {
split := strings.SplitN(directive, "=", 2) split := strings.SplitN(directive, "=", 2)
key := split[0] key := split[0]
if len(split) == 1 {
return fmt.Errorf("invalid directive format in %q, should be '# +key=value'", line)
}
value := split[1]
switch key { switch key {
case ExpiryTimeKey: case DirectiveExpiryTime:
if len(split) == 1 {
return fmt.Errorf("invalid directive format in %q, should be '# +expiry-time=value'", line)
}
value := split[1]
expiryDate, err := time.Parse(time.RFC3339, strings.TrimSpace(value)) expiryDate, err := time.Parse(time.RFC3339, strings.TrimSpace(value))
if err != nil { if err != nil {
return fmt.Errorf("failed to parse expiry-date directive: %w", err) return fmt.Errorf("failed to parse expiry-date directive: %w", err)
} }
opts.ExpiryTime = expiryDate opts.ExpiryTime = expiryDate
case DirectiveNoFeature:
opts.SkipFeature = true
case DirectiveNoLabel:
opts.SkipLabel = true
default: default:
return fmt.Errorf("unknown feature file directive %q", key) return fmt.Errorf("unknown feature file directive %q", key)
} }
@ -188,11 +215,15 @@ func parseDirectives(line string, opts *parsingOpts) error {
return nil return nil
} }
func parseFeatures(lines [][]byte, fileName string) map[string]string { func parseFeatureFile(lines [][]byte, fileName string) (map[string]string, map[string]string) {
features := make(map[string]string) features := make(map[string]string)
labels := make(map[string]string)
now := time.Now() now := time.Now()
parsingOpts := &parsingOpts{ parsingOpts := &parsingOpts{
ExpiryTime: now, ExpiryTime: now,
SkipLabel: false,
SkipFeature: false,
} }
for _, l := range lines { for _, l := range lines {
@ -217,30 +248,50 @@ func parseFeatures(lines [][]byte, fileName string) map[string]string {
key := lineSplit[0] key := lineSplit[0]
// Check if it's a boolean value if !parsingOpts.SkipFeature {
if len(lineSplit) == 1 { updateFeatures(features, lineSplit)
features[key] = "true"
} else { } else {
features[key] = lineSplit[1] delete(features, key)
} }
if !parsingOpts.SkipLabel {
updateFeatures(labels, lineSplit)
} else {
delete(labels, key)
}
// SkipFeature and SkipLabel only take effect for one feature
parsingOpts.SkipFeature = false
parsingOpts.SkipLabel = false
} }
} }
return features return features, labels
}
func updateFeatures(m map[string]string, lineSplit []string) {
key := lineSplit[0]
// Check if it's a boolean value
if len(lineSplit) == 1 {
m[key] = "true"
} else {
m[key] = lineSplit[1]
}
} }
// Run all hooks and get features // Run all hooks and get features
func getFeaturesFromHooks() (map[string]string, error) { func getFeaturesFromHooks() (map[string]string, map[string]string, error) {
features := make(map[string]string) features := make(map[string]string)
labels := make(map[string]string)
files, err := os.ReadDir(hookDir) files, err := os.ReadDir(hookDir)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
klog.InfoS("hook directory does not exist", "path", hookDir) klog.InfoS("hook directory does not exist", "path", hookDir)
return features, nil return features, labels, nil
} }
return features, fmt.Errorf("unable to access %v: %v", hookDir, err) return features, labels, fmt.Errorf("unable to access %v: %v", hookDir, err)
} }
if len(files) > 0 { if len(files) > 0 {
klog.InfoS("hooks are DEPRECATED since v0.12.0 and support will be removed in a future release; use feature files instead") klog.InfoS("hooks are DEPRECATED since v0.12.0 and support will be removed in a future release; use feature files instead")
@ -259,17 +310,24 @@ func getFeaturesFromHooks() (map[string]string, error) {
} }
// Append features // Append features
fileFeatures := parseFeatures(lines, fileName) fileFeatures, fileLabels := parseFeatureFile(lines, fileName)
klog.V(4).InfoS("hook executed", "fileName", fileName, "features", utils.DelayedDumper(fileFeatures)) klog.V(4).InfoS("hook executed", "fileName", fileName, "features", utils.DelayedDumper(fileFeatures), "labels", utils.DelayedDumper(fileLabels))
for k, v := range fileFeatures { for k, v := range fileFeatures {
if old, ok := features[k]; ok { if old, ok := features[k]; ok {
klog.InfoS("overriding label value from another hook", "labelKey", k, "oldValue", old, "newValue", v, "fileName", fileName) klog.InfoS("overriding feature value from another hook", "featureKey", k, "oldValue", old, "newValue", v, "fileName", fileName)
} }
features[k] = v features[k] = v
} }
for k, v := range fileLabels {
if old, ok := labels[k]; ok {
klog.InfoS("overriding label value from another hook", "labelKey", k, "oldValue", old, "newValue", v, "fileName", fileName)
}
labels[k] = v
}
} }
return features, nil return features, labels, nil
} }
// Run one hook // Run one hook
@ -314,16 +372,17 @@ func runHook(file string) ([][]byte, error) {
} }
// Read all files to get features // Read all files to get features
func getFeaturesFromFiles() (map[string]string, error) { func getFeaturesFromFiles() (map[string]string, map[string]string, error) {
features := make(map[string]string) features := make(map[string]string)
labels := make(map[string]string)
files, err := os.ReadDir(featureFilesDir) files, err := os.ReadDir(featureFilesDir)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
klog.InfoS("features directory does not exist", "path", featureFilesDir) klog.InfoS("features directory does not exist", "path", featureFilesDir)
return features, nil return features, labels, nil
} }
return features, fmt.Errorf("unable to access %v: %v", featureFilesDir, err) return features, labels, fmt.Errorf("unable to access %v: %v", featureFilesDir, err)
} }
for _, file := range files { for _, file := range files {
@ -339,18 +398,25 @@ func getFeaturesFromFiles() (map[string]string, error) {
} }
// Append features // Append features
fileFeatures := parseFeatures(lines, fileName) fileFeatures, fileLabels := parseFeatureFile(lines, fileName)
klog.V(4).InfoS("feature file read", "fileName", fileName, "features", utils.DelayedDumper(fileFeatures)) klog.V(4).InfoS("feature file read", "fileName", fileName, "features", utils.DelayedDumper(fileFeatures))
for k, v := range fileFeatures { for k, v := range fileFeatures {
if old, ok := features[k]; ok { if old, ok := features[k]; ok {
klog.InfoS("overriding label value from another feature file", "labelKey", k, "oldValue", old, "newValue", v, "fileName", fileName) klog.InfoS("overriding label value from another feature file", "featureKey", k, "oldValue", old, "newValue", v, "fileName", fileName)
} }
features[k] = v features[k] = v
} }
for k, v := range fileLabels {
if old, ok := labels[k]; ok {
klog.InfoS("overriding label value from another feature file", "labelKey", k, "oldValue", old, "newValue", v, "fileName", fileName)
}
labels[k] = v
}
} }
return features, nil return features, labels, nil
} }
// Read one file // Read one file

View file

@ -17,7 +17,6 @@ limitations under the License.
package local package local
import ( import (
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
@ -39,14 +38,16 @@ func TestLocalSource(t *testing.T) {
} }
func TestGetExpirationDate(t *testing.T) { func TestGetExpirationDate(t *testing.T) {
expectedFeaturesLen := 5 expectedFeaturesLen := 7
expectedLabelsLen := 8
pwd, _ := os.Getwd() pwd, _ := os.Getwd()
featureFilesDir = filepath.Join(pwd, "testdata/features.d") featureFilesDir = filepath.Join(pwd, "testdata/features.d")
features, labels, err := getFeaturesFromFiles()
features, err := getFeaturesFromFiles()
fmt.Println(features)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, expectedFeaturesLen, len(features)) assert.Equal(t, expectedFeaturesLen, len(features))
assert.Equal(t, expectedLabelsLen, len(labels))
} }
func TestParseDirectives(t *testing.T) { func TestParseDirectives(t *testing.T) {

View file

@ -0,0 +1,11 @@
# +no-feature
label-only=value
# +no-feature
label-only-2=value
my-feature=value
foo=bar
# +no-label
foo=baz