Skip to content

Commit 0323fdf

Browse files
committed
feat: add ability to export
For the HPC use case, we want to be able to export static features, raw or as labels, to the terminal or a JSON path. Signed-off-by: vsoch <[email protected]>
1 parent 310a390 commit 0323fdf

File tree

8 files changed

+307
-4
lines changed

8 files changed

+307
-4
lines changed

cmd/nfd/subcmd/export/export.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package export
18+
19+
import (
20+
"fmt"
21+
"os"
22+
23+
"github.com/spf13/cobra"
24+
)
25+
26+
var ExportCmd = &cobra.Command{
27+
Use: "export",
28+
Short: "Export commands",
29+
}
30+
31+
func Execute() {
32+
if err := ExportCmd.Execute(); err != nil {
33+
fmt.Println(err)
34+
os.Exit(1)
35+
}
36+
}

cmd/nfd/subcmd/export/features.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package export
18+
19+
import (
20+
"encoding/json"
21+
"fmt"
22+
23+
"github.com/spf13/cobra"
24+
25+
"sigs.k8s.io/node-feature-discovery/source"
26+
)
27+
28+
func NewExportCmd() *cobra.Command {
29+
cmd := &cobra.Command{
30+
Use: "features",
31+
Short: "Export features for given node",
32+
RunE: func(cmd *cobra.Command, args []string) error {
33+
sources := map[string]source.FeatureSource{}
34+
for k, v := range source.GetAllFeatureSources() {
35+
if ts, ok := v.(source.SupplementalSource); ok && ts.DisableByDefault() {
36+
continue
37+
}
38+
sources[k] = v
39+
}
40+
41+
// Discover all feature sources
42+
for _, s := range sources {
43+
if err := s.Discover(); err != nil {
44+
return fmt.Errorf("error during discovery of source %s: %w", s.Name(), err)
45+
}
46+
}
47+
48+
features := source.GetAllFeatures()
49+
exportedLabels, err := json.MarshalIndent(features, "", " ")
50+
if err != nil {
51+
return err
52+
}
53+
54+
if outputPath != "" {
55+
err = writeToFile(outputPath, string(exportedLabels))
56+
} else {
57+
fmt.Println(string(exportedLabels))
58+
}
59+
return err
60+
},
61+
}
62+
cmd.Flags().StringVar(&outputPath, "path", "", "export to this JSON path")
63+
return cmd
64+
}
65+
66+
func init() {
67+
ExportCmd.AddCommand(NewExportCmd())
68+
}

cmd/nfd/subcmd/export/labels.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package export
18+
19+
import (
20+
"encoding/json"
21+
"fmt"
22+
"maps"
23+
"regexp"
24+
"slices"
25+
"sort"
26+
27+
"github.com/spf13/cobra"
28+
"k8s.io/klog/v2"
29+
30+
worker "sigs.k8s.io/node-feature-discovery/pkg/nfd-worker"
31+
"sigs.k8s.io/node-feature-discovery/source"
32+
)
33+
34+
func NewLabelsCmd() *cobra.Command {
35+
cmd := &cobra.Command{
36+
Use: "labels",
37+
Short: "Export feature labels for given node",
38+
RunE: func(cmd *cobra.Command, args []string) error {
39+
40+
// Determine enabled feature sources
41+
featureSources := make(map[string]source.FeatureSource)
42+
for n, s := range source.GetAllFeatureSources() {
43+
if ts, ok := s.(source.SupplementalSource); !ok || !ts.DisableByDefault() {
44+
err := s.Discover()
45+
if err != nil {
46+
return err
47+
}
48+
featureSources[n] = s
49+
}
50+
}
51+
featureSourceList := slices.Collect(maps.Values(featureSources))
52+
sort.Slice(featureSourceList, func(i, j int) bool { return featureSourceList[i].Name() < featureSourceList[j].Name() })
53+
54+
// Determine enabled label sources
55+
labelSources := make(map[string]source.LabelSource)
56+
for n, s := range source.GetAllLabelSources() {
57+
if ts, ok := s.(source.SupplementalSource); !ok || !ts.DisableByDefault() {
58+
labelSources[n] = s
59+
}
60+
}
61+
labelSourcesList := slices.Collect(maps.Values(labelSources))
62+
sort.Slice(labelSourcesList, func(i, j int) bool {
63+
iP, jP := labelSourcesList[i].Priority(), labelSourcesList[j].Priority()
64+
if iP != jP {
65+
return iP < jP
66+
}
67+
return labelSourcesList[i].Name() < labelSourcesList[j].Name()
68+
})
69+
70+
labels := worker.Labels{}
71+
labelWhiteList := *regexp.MustCompile("")
72+
73+
// Get labels from all enabled label sources
74+
for _, source := range labelSourcesList {
75+
labelsFromSource, err := worker.GetFeatureLabels(source, labelWhiteList)
76+
if err != nil {
77+
klog.ErrorS(err, "discovery failed", "source", source.Name())
78+
continue
79+
}
80+
maps.Copy(labels, labelsFromSource)
81+
}
82+
83+
exportedLabels, err := json.MarshalIndent(labels, "", " ")
84+
if err != nil {
85+
return err
86+
}
87+
88+
if outputPath != "" {
89+
err = writeToFile(outputPath, string(exportedLabels))
90+
} else {
91+
fmt.Println(string(exportedLabels))
92+
}
93+
return err
94+
},
95+
}
96+
cmd.Flags().StringVar(&outputPath, "path", "", "export to this JSON path")
97+
return cmd
98+
}
99+
100+
func init() {
101+
ExportCmd.AddCommand(NewLabelsCmd())
102+
}

cmd/nfd/subcmd/export/utils.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package export
18+
19+
import (
20+
"fmt"
21+
"os"
22+
)
23+
24+
var (
25+
outputPath string
26+
)
27+
28+
// writeToFile saves string content to a file at the path set by path
29+
func writeToFile(path, content string) error {
30+
fd, err := os.Create(path)
31+
if err != nil {
32+
return err
33+
}
34+
defer fd.Close()
35+
_, err = fmt.Fprint(fd, content)
36+
return err
37+
}

cmd/nfd/subcmd/root.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/spf13/cobra"
2424

2525
"sigs.k8s.io/node-feature-discovery/cmd/nfd/subcmd/compat"
26+
"sigs.k8s.io/node-feature-discovery/cmd/nfd/subcmd/export"
2627
)
2728

2829
// RootCmd represents the base command when called without any subcommands
@@ -33,6 +34,7 @@ var RootCmd = &cobra.Command{
3334

3435
func init() {
3536
RootCmd.AddCommand(compat.CompatCmd)
37+
RootCmd.AddCommand(export.ExportCmd)
3638
}
3739

3840
// Execute adds all child commands to the root command and sets flags appropriately.

docs/usage/nfd-export.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
title: "Export"
3+
layout: default
4+
sort: 12
5+
---
6+
7+
# Feature Export
8+
{: .no_toc}
9+
10+
## Table of contents
11+
{: .no_toc .text-delta}
12+
13+
1. TOC
14+
{:toc}
15+
16+
---
17+
18+
## Export
19+
20+
If you are interested in exporting features or labels in a generic
21+
context, the nfd client supports an export mode, where both can be
22+
derived on the command line.
23+
24+
### Features
25+
26+
**Feature export is in the experimental version.**
27+
28+
This addresses use cases such as high performance computing (HPC) and
29+
other environments with compute nodes that warrant assessment, but may
30+
not have Kubernetes running, or may not be able to or want to run a
31+
central daemon service for data. To export features, you can use `nfd
32+
export features`:
33+
34+
```bash
35+
nfd export features
36+
```
37+
38+
By default, JSON structure with parsed key value pairs will appear in the
39+
terminal. To save to a file path:
40+
41+
```bash
42+
nfd export features --path features.json
43+
```
44+
45+
### Labels
46+
47+
To export equivalent labels outside of a Kubernetes context,
48+
you can use `nfd export labels`.
49+
50+
```bash
51+
nfd export labels
52+
```
53+
54+
Or export to an output file:
55+
56+
```bash
57+
nfd export labels --path labels.json
58+
```

pkg/nfd-worker/nfd-worker-internal_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func TestGetLabelsWithMockSources(t *testing.T) {
5454
mockLabelSource.On("Name").Return(fakeLabelSourceName)
5555
mockLabelSource.On("GetLabels").Return(fakeFeatures, nil)
5656

57-
returnedLabels, err := getFeatureLabels(fakeLabelSource, labelWhiteList.Regexp)
57+
returnedLabels, err := GetFeatureLabels(fakeLabelSource, labelWhiteList.Regexp)
5858
Convey("Proper label is returned", func() {
5959
So(returnedLabels, ShouldResemble, fakeFeatureLabels)
6060
})
@@ -67,7 +67,7 @@ func TestGetLabelsWithMockSources(t *testing.T) {
6767
expectedError := errors.New("fake error")
6868
mockLabelSource.On("GetLabels").Return(nil, expectedError)
6969

70-
returnedLabels, err := getFeatureLabels(fakeLabelSource, labelWhiteList.Regexp)
70+
returnedLabels, err := GetFeatureLabels(fakeLabelSource, labelWhiteList.Regexp)
7171
Convey("No label is returned", func() {
7272
So(returnedLabels, ShouldBeNil)
7373
})

pkg/nfd-worker/nfd-worker.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -537,7 +537,7 @@ func createFeatureLabels(sources []source.LabelSource, labelWhiteList regexp.Reg
537537
// Get labels from all enabled label sources
538538
klog.InfoS("starting feature discovery...")
539539
for _, source := range sources {
540-
labelsFromSource, err := getFeatureLabels(source, labelWhiteList)
540+
labelsFromSource, err := GetFeatureLabels(source, labelWhiteList)
541541
if err != nil {
542542
klog.ErrorS(err, "discovery failed", "source", source.Name())
543543
continue
@@ -555,7 +555,7 @@ func createFeatureLabels(sources []source.LabelSource, labelWhiteList regexp.Reg
555555

556556
// getFeatureLabels returns node labels for features discovered by the
557557
// supplied source.
558-
func getFeatureLabels(source source.LabelSource, labelWhiteList regexp.Regexp) (labels Labels, err error) {
558+
func GetFeatureLabels(source source.LabelSource, labelWhiteList regexp.Regexp) (labels Labels, err error) {
559559
labels = Labels{}
560560
features, err := source.GetLabels()
561561
if err != nil {

0 commit comments

Comments
 (0)