Skip to content

Commit 4b3429d

Browse files
authored
Merge pull request #1386 from AhmedGrati/feat-support-raw-features
feat: support raw features
2 parents dd8d7f6 + 3130898 commit 4b3429d

File tree

4 files changed

+153
-39
lines changed

4 files changed

+153
-39
lines changed

docs/usage/customization-guide.md

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,41 @@ included in the list of accepted features.
355355
> tag is only evaluated in each re-discovery period, and the expiration of
356356
> node labels is not tracked.
357357

358+
To exclude specific features from the `local.feature` Feature, you can use the
359+
`# +no-feature` directive. The `# +no-label` directive causes the feature to
360+
be excluded from the `local.label` Feature and a node label not to be generated.
361+
362+
Considering the following file:
363+
364+
```plaintext
365+
# +no-feature
366+
label-only=value
367+
368+
my-feature=value
369+
370+
foo=bar
371+
# +no-label
372+
foo=baz
373+
```
374+
375+
Processing the above file would result in the following Features:
376+
377+
```yaml
378+
local.features:
379+
foo: baz
380+
my-feature: value
381+
local.labels:
382+
label-only: value
383+
my-feature: value
384+
```
385+
386+
and the following labels added to the Node:
387+
388+
```plaintext
389+
feature.node.kubernetes.io/label-only=value
390+
feature.node.kubernetes.io/my-feature=value
391+
```
392+
358393
### Mounts
359394

360395
The standard NFD deployments contain `hostPath` mounts for
@@ -775,7 +810,8 @@ The following features are available for matching:
775810
| | | **`major`** | int | First component of the kernel version (e.g. ‘4')
776811
| | | **`minor`** | int | Second component of the kernel version (e.g. ‘5')
777812
| | | **`revision`** | int | Third component of the kernel version (e.g. ‘6')
778-
| **`local.label`** | attribute | | | Features feature files and hooks, i.e. labels from the [*local* feature source](#local-feature-source)
813+
| **`local.label`** | attribute | | | Labels from feature files and hooks, i.e. labels from the [*local* feature source](#local-feature-source)
814+
| **`local.feature`** | attribute | | | Features from feature files and hooks, i.e. features from the [*local* feature source](#local-feature-source)
779815
| | | **`<label-name>`** | string | Label `<label-name>` created by the local feature source, value equals the value of the label
780816
| **`memory.nv`** | instance | | | NVDIMM devices present in the system
781817
| | | **`<sysfs-attribute>`** | string | Value of the sysfs device attribute, available attributes: `devtype`, `mode`

source/local/local.go

Lines changed: 100 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,22 @@ const Name = "local"
3838
// LabelFeature of this feature source
3939
const LabelFeature = "label"
4040

41-
// ExpiryTimeKey is the key of this feature source indicating
42-
// when features should be removed.
43-
const ExpiryTimeKey = "expiry-time"
41+
// RawFeature of this feature source
42+
const RawFeature = "feature"
43+
44+
const (
45+
// ExpiryTimeKey is the key of this feature source indicating
46+
// when features should be removed.
47+
DirectiveExpiryTime = "expiry-time"
48+
49+
// NoLabel indicates whether the feature should be included
50+
// in exposed labels or not.
51+
DirectiveNoLabel = "no-label"
52+
53+
// NoFeature indicates whether the feature should be included
54+
// in exposed raw features or not.
55+
DirectiveNoFeature = "no-feature"
56+
)
4457

4558
// DirectivePrefix defines the prefix of directives that should be parsed
4659
const DirectivePrefix = "# +"
@@ -66,7 +79,9 @@ type Config struct {
6679

6780
// parsingOpts contains options used for directives parsing
6881
type parsingOpts struct {
69-
ExpiryTime time.Time
82+
ExpiryTime time.Time
83+
SkipLabel bool
84+
SkipFeature bool
7085
}
7186

7287
// Singleton source instance
@@ -121,7 +136,7 @@ func newDefaultConfig() *Config {
121136
func (s *localSource) Discover() error {
122137
s.features = nfdv1alpha1.NewFeatures()
123138

124-
featuresFromFiles, err := getFeaturesFromFiles()
139+
featuresFromFiles, labelsFromFiles, err := getFeaturesFromFiles()
125140
if err != nil {
126141
klog.ErrorS(err, "failed to read feature files")
127142
}
@@ -131,21 +146,30 @@ func (s *localSource) Discover() error {
131146
klog.InfoS("starting hooks...")
132147
klog.InfoS("NOTE: hooks are deprecated and will be completely removed in a future release.")
133148

134-
featuresFromHooks, err := getFeaturesFromHooks()
149+
featuresFromHooks, labelsFromHooks, err := getFeaturesFromHooks()
135150
if err != nil {
136151
klog.ErrorS(err, "failed to run hooks")
137152
}
138153

139154
// Merge features from hooks and files
140155
for k, v := range featuresFromHooks {
141156
if old, ok := featuresFromFiles[k]; ok {
142-
klog.InfoS("overriding label value", "labelKey", k, "oldValue", old, "newValue", v)
157+
klog.InfoS("overriding feature value", "featureKey", k, "oldValue", old, "newValue", v)
143158
}
144159
featuresFromFiles[k] = v
145160
}
161+
162+
// Merge labels from hooks and files
163+
for k, v := range labelsFromHooks {
164+
if old, ok := labelsFromFiles[k]; ok {
165+
klog.InfoS("overriding label value", "labelKey", k, "oldValue", old, "newValue", v)
166+
}
167+
labelsFromHooks[k] = v
168+
}
146169
}
147170

148-
s.features.Attributes[LabelFeature] = nfdv1alpha1.NewAttributeFeatures(featuresFromFiles)
171+
s.features.Attributes[LabelFeature] = nfdv1alpha1.NewAttributeFeatures(labelsFromFiles)
172+
s.features.Attributes[RawFeature] = nfdv1alpha1.NewAttributeFeatures(featuresFromFiles)
149173

150174
klog.V(3).InfoS("discovered features", "featureSource", s.Name(), "features", utils.DelayedDumper(s.features))
151175

@@ -169,30 +193,37 @@ func parseDirectives(line string, opts *parsingOpts) error {
169193
split := strings.SplitN(directive, "=", 2)
170194
key := split[0]
171195

172-
if len(split) == 1 {
173-
return fmt.Errorf("invalid directive format in %q, should be '# +key=value'", line)
174-
}
175-
value := split[1]
176-
177196
switch key {
178-
case ExpiryTimeKey:
197+
case DirectiveExpiryTime:
198+
if len(split) == 1 {
199+
return fmt.Errorf("invalid directive format in %q, should be '# +expiry-time=value'", line)
200+
}
201+
value := split[1]
179202
expiryDate, err := time.Parse(time.RFC3339, strings.TrimSpace(value))
180203
if err != nil {
181204
return fmt.Errorf("failed to parse expiry-date directive: %w", err)
182205
}
183206
opts.ExpiryTime = expiryDate
207+
case DirectiveNoFeature:
208+
opts.SkipFeature = true
209+
case DirectiveNoLabel:
210+
opts.SkipLabel = true
184211
default:
185212
return fmt.Errorf("unknown feature file directive %q", key)
186213
}
187214

188215
return nil
189216
}
190217

191-
func parseFeatures(lines [][]byte, fileName string) map[string]string {
218+
func parseFeatureFile(lines [][]byte, fileName string) (map[string]string, map[string]string) {
192219
features := make(map[string]string)
220+
labels := make(map[string]string)
221+
193222
now := time.Now()
194223
parsingOpts := &parsingOpts{
195-
ExpiryTime: now,
224+
ExpiryTime: now,
225+
SkipLabel: false,
226+
SkipFeature: false,
196227
}
197228

198229
for _, l := range lines {
@@ -217,30 +248,50 @@ func parseFeatures(lines [][]byte, fileName string) map[string]string {
217248

218249
key := lineSplit[0]
219250

220-
// Check if it's a boolean value
221-
if len(lineSplit) == 1 {
222-
features[key] = "true"
251+
if !parsingOpts.SkipFeature {
252+
updateFeatures(features, lineSplit)
253+
} else {
254+
delete(features, key)
255+
}
256+
257+
if !parsingOpts.SkipLabel {
258+
updateFeatures(labels, lineSplit)
223259
} else {
224-
features[key] = lineSplit[1]
260+
delete(labels, key)
225261
}
262+
// SkipFeature and SkipLabel only take effect for one feature
263+
parsingOpts.SkipFeature = false
264+
parsingOpts.SkipLabel = false
226265
}
227266
}
228267

229-
return features
268+
return features, labels
269+
}
270+
271+
func updateFeatures(m map[string]string, lineSplit []string) {
272+
key := lineSplit[0]
273+
// Check if it's a boolean value
274+
if len(lineSplit) == 1 {
275+
m[key] = "true"
276+
277+
} else {
278+
m[key] = lineSplit[1]
279+
}
230280
}
231281

232282
// Run all hooks and get features
233-
func getFeaturesFromHooks() (map[string]string, error) {
283+
func getFeaturesFromHooks() (map[string]string, map[string]string, error) {
234284

235285
features := make(map[string]string)
286+
labels := make(map[string]string)
236287

237288
files, err := os.ReadDir(hookDir)
238289
if err != nil {
239290
if os.IsNotExist(err) {
240291
klog.InfoS("hook directory does not exist", "path", hookDir)
241-
return features, nil
292+
return features, labels, nil
242293
}
243-
return features, fmt.Errorf("unable to access %v: %v", hookDir, err)
294+
return features, labels, fmt.Errorf("unable to access %v: %v", hookDir, err)
244295
}
245296
if len(files) > 0 {
246297
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) {
259310
}
260311

261312
// Append features
262-
fileFeatures := parseFeatures(lines, fileName)
263-
klog.V(4).InfoS("hook executed", "fileName", fileName, "features", utils.DelayedDumper(fileFeatures))
313+
fileFeatures, fileLabels := parseFeatureFile(lines, fileName)
314+
klog.V(4).InfoS("hook executed", "fileName", fileName, "features", utils.DelayedDumper(fileFeatures), "labels", utils.DelayedDumper(fileLabels))
264315
for k, v := range fileFeatures {
265316
if old, ok := features[k]; ok {
266-
klog.InfoS("overriding label value from another hook", "labelKey", k, "oldValue", old, "newValue", v, "fileName", fileName)
317+
klog.InfoS("overriding feature value from another hook", "featureKey", k, "oldValue", old, "newValue", v, "fileName", fileName)
267318
}
268319
features[k] = v
269320
}
321+
322+
for k, v := range fileLabels {
323+
if old, ok := labels[k]; ok {
324+
klog.InfoS("overriding label value from another hook", "labelKey", k, "oldValue", old, "newValue", v, "fileName", fileName)
325+
}
326+
labels[k] = v
327+
}
270328
}
271329

272-
return features, nil
330+
return features, labels, nil
273331
}
274332

275333
// Run one hook
@@ -314,16 +372,17 @@ func runHook(file string) ([][]byte, error) {
314372
}
315373

316374
// Read all files to get features
317-
func getFeaturesFromFiles() (map[string]string, error) {
375+
func getFeaturesFromFiles() (map[string]string, map[string]string, error) {
318376
features := make(map[string]string)
377+
labels := make(map[string]string)
319378

320379
files, err := os.ReadDir(featureFilesDir)
321380
if err != nil {
322381
if os.IsNotExist(err) {
323382
klog.InfoS("features directory does not exist", "path", featureFilesDir)
324-
return features, nil
383+
return features, labels, nil
325384
}
326-
return features, fmt.Errorf("unable to access %v: %v", featureFilesDir, err)
385+
return features, labels, fmt.Errorf("unable to access %v: %v", featureFilesDir, err)
327386
}
328387

329388
for _, file := range files {
@@ -339,18 +398,25 @@ func getFeaturesFromFiles() (map[string]string, error) {
339398
}
340399

341400
// Append features
342-
fileFeatures := parseFeatures(lines, fileName)
401+
fileFeatures, fileLabels := parseFeatureFile(lines, fileName)
343402

344403
klog.V(4).InfoS("feature file read", "fileName", fileName, "features", utils.DelayedDumper(fileFeatures))
345404
for k, v := range fileFeatures {
346405
if old, ok := features[k]; ok {
347-
klog.InfoS("overriding label value from another feature file", "labelKey", k, "oldValue", old, "newValue", v, "fileName", fileName)
406+
klog.InfoS("overriding label value from another feature file", "featureKey", k, "oldValue", old, "newValue", v, "fileName", fileName)
348407
}
349408
features[k] = v
350409
}
410+
411+
for k, v := range fileLabels {
412+
if old, ok := labels[k]; ok {
413+
klog.InfoS("overriding label value from another feature file", "labelKey", k, "oldValue", old, "newValue", v, "fileName", fileName)
414+
}
415+
labels[k] = v
416+
}
351417
}
352418

353-
return features, nil
419+
return features, labels, nil
354420
}
355421

356422
// Read one file

source/local/local_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ limitations under the License.
1717
package local
1818

1919
import (
20-
"fmt"
2120
"os"
2221
"path/filepath"
2322
"testing"
@@ -39,14 +38,16 @@ func TestLocalSource(t *testing.T) {
3938
}
4039

4140
func TestGetExpirationDate(t *testing.T) {
42-
expectedFeaturesLen := 5
41+
expectedFeaturesLen := 7
42+
expectedLabelsLen := 8
43+
4344
pwd, _ := os.Getwd()
4445
featureFilesDir = filepath.Join(pwd, "testdata/features.d")
46+
features, labels, err := getFeaturesFromFiles()
4547

46-
features, err := getFeaturesFromFiles()
47-
fmt.Println(features)
4848
assert.NoError(t, err)
4949
assert.Equal(t, expectedFeaturesLen, len(features))
50+
assert.Equal(t, expectedLabelsLen, len(labels))
5051
}
5152

5253
func TestParseDirectives(t *testing.T) {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# +no-feature
2+
label-only=value
3+
4+
# +no-feature
5+
label-only-2=value
6+
7+
my-feature=value
8+
9+
foo=bar
10+
# +no-label
11+
foo=baz

0 commit comments

Comments
 (0)