Skip to content

Commit 341bdb6

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 341bdb6

File tree

7 files changed

+278
-4
lines changed

7 files changed

+278
-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 2024 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: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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+
"os"
23+
24+
"github.com/spf13/cobra"
25+
26+
"sigs.k8s.io/node-feature-discovery/source"
27+
)
28+
29+
var (
30+
outputPath string
31+
)
32+
33+
func NewExportCmd() *cobra.Command {
34+
cmd := &cobra.Command{
35+
Use: "features",
36+
Short: "Export features for given node",
37+
RunE: func(cmd *cobra.Command, args []string) error {
38+
sources := map[string]source.FeatureSource{}
39+
for k, v := range source.GetAllFeatureSources() {
40+
if ts, ok := v.(source.SupplementalSource); ok && ts.DisableByDefault() {
41+
continue
42+
}
43+
sources[k] = v
44+
}
45+
46+
// Discover all feature sources
47+
for _, s := range sources {
48+
if err := s.Discover(); err != nil {
49+
return fmt.Errorf("error during discovery of source %s: %w", s.Name(), err)
50+
}
51+
}
52+
53+
features := source.GetAllFeatures()
54+
exportedLabels, err := json.MarshalIndent(features, "", " ")
55+
if err != nil {
56+
return err
57+
}
58+
59+
if outputPath != "" {
60+
fd, err := os.Create(outputPath)
61+
if err != nil {
62+
return err
63+
}
64+
defer fd.Close()
65+
_, err = fmt.Fprint(fd, string(exportedLabels))
66+
return err
67+
} else {
68+
fmt.Println(string(exportedLabels))
69+
}
70+
return nil
71+
},
72+
}
73+
cmd.Flags().StringVar(&outputPath, "path", "", "export to this JSON path")
74+
return cmd
75+
}
76+
77+
func init() {
78+
ExportCmd.AddCommand(NewExportCmd())
79+
}

cmd/nfd/subcmd/export/labels.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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+
"os"
24+
"regexp"
25+
"slices"
26+
"sort"
27+
28+
"github.com/spf13/cobra"
29+
"k8s.io/klog/v2"
30+
31+
worker "sigs.k8s.io/node-feature-discovery/pkg/nfd-worker"
32+
"sigs.k8s.io/node-feature-discovery/source"
33+
)
34+
35+
func NewLabelsCmd() *cobra.Command {
36+
cmd := &cobra.Command{
37+
Use: "labels",
38+
Short: "Export feature labels for given node",
39+
RunE: func(cmd *cobra.Command, args []string) error {
40+
41+
// Determine enabled feature sources
42+
featureSources := make(map[string]source.FeatureSource)
43+
for n, s := range source.GetAllFeatureSources() {
44+
if ts, ok := s.(source.SupplementalSource); !ok || !ts.DisableByDefault() {
45+
s.Discover()
46+
featureSources[n] = s
47+
}
48+
}
49+
featureSourceList := slices.Collect(maps.Values(featureSources))
50+
sort.Slice(featureSourceList, func(i, j int) bool { return featureSourceList[i].Name() < featureSourceList[j].Name() })
51+
52+
// Determine enabled label sources
53+
labelSources := make(map[string]source.LabelSource)
54+
for n, s := range source.GetAllLabelSources() {
55+
if ts, ok := s.(source.SupplementalSource); !ok || !ts.DisableByDefault() {
56+
labelSources[n] = s
57+
}
58+
}
59+
labelSourcesList := slices.Collect(maps.Values(labelSources))
60+
sort.Slice(labelSourcesList, func(i, j int) bool {
61+
iP, jP := labelSourcesList[i].Priority(), labelSourcesList[j].Priority()
62+
if iP != jP {
63+
return iP < jP
64+
}
65+
return labelSourcesList[i].Name() < labelSourcesList[j].Name()
66+
})
67+
68+
labels := worker.Labels{}
69+
labelWhiteList := *regexp.MustCompile("")
70+
71+
// Get labels from all enabled label sources
72+
for _, source := range labelSourcesList {
73+
labelsFromSource, err := worker.GetFeatureLabels(source, labelWhiteList)
74+
if err != nil {
75+
klog.ErrorS(err, "discovery failed", "source", source.Name())
76+
continue
77+
}
78+
maps.Copy(labels, labelsFromSource)
79+
}
80+
81+
exportedLabels, err := json.MarshalIndent(labels, "", " ")
82+
if err != nil {
83+
return err
84+
}
85+
86+
if outputPath != "" {
87+
fd, err := os.Create(outputPath)
88+
if err != nil {
89+
return err
90+
}
91+
defer fd.Close()
92+
_, err = fmt.Fprint(fd, string(exportedLabels))
93+
return err
94+
} else {
95+
fmt.Println(string(exportedLabels))
96+
}
97+
return nil
98+
},
99+
}
100+
cmd.Flags().StringVar(&outputPath, "path", "", "export to this JSON path")
101+
return cmd
102+
}
103+
104+
func init() {
105+
ExportCmd.AddCommand(NewLabelsCmd())
106+
}

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: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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 context, the nfd client supports an export mode, where both can be derived on the command line.
21+
22+
### Feature Export
23+
24+
**Feature export is in the experimental `v1alpha1` version.**
25+
26+
This addresses use cases such as high performance computing (HPC) and other environments with compute nodes that warrant assessment, but may not have Kubernetes running, or may not be able to or want to run a central daemon service for data. To export features, you can use `nfd export features`:
27+
28+
```bash
29+
nfd export features
30+
```
31+
32+
By default, JSON structure with parsed key value pairs will appear in the terminal.
33+
To save to a file path:
34+
35+
```bash
36+
nfd export features --path features.json
37+
```
38+
39+
### Label Export
40+
41+
To export equivalent labels outside of a Kubernetes context, you can use `nfd export labels`.
42+
43+
```bash
44+
nfd export labels
45+
```
46+
47+
Or export to an output file:
48+
49+
```bash
50+
nfd export labels --path labels.json
51+
```

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)