diff --git a/cmd/nfd/subcmd/export/export.go b/cmd/nfd/subcmd/export/export.go new file mode 100644 index 0000000000..9027e36d40 --- /dev/null +++ b/cmd/nfd/subcmd/export/export.go @@ -0,0 +1,36 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package export + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +var ExportCmd = &cobra.Command{ + Use: "export", + Short: "Export commands", +} + +func Execute() { + if err := ExportCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/cmd/nfd/subcmd/export/features.go b/cmd/nfd/subcmd/export/features.go new file mode 100644 index 0000000000..8085012ed7 --- /dev/null +++ b/cmd/nfd/subcmd/export/features.go @@ -0,0 +1,68 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package export + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + + "sigs.k8s.io/node-feature-discovery/source" +) + +func NewExportCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "features", + Short: "Export features for given node", + RunE: func(cmd *cobra.Command, args []string) error { + sources := map[string]source.FeatureSource{} + for k, v := range source.GetAllFeatureSources() { + if ts, ok := v.(source.SupplementalSource); ok && ts.DisableByDefault() { + continue + } + sources[k] = v + } + + // Discover all feature sources + for _, s := range sources { + if err := s.Discover(); err != nil { + return fmt.Errorf("error during discovery of source %s: %w", s.Name(), err) + } + } + + features := source.GetAllFeatures() + exportedLabels, err := json.MarshalIndent(features, "", " ") + if err != nil { + return err + } + + if outputPath != "" { + err = writeToFile(outputPath, string(exportedLabels)) + } else { + fmt.Println(string(exportedLabels)) + } + return err + }, + } + cmd.Flags().StringVar(&outputPath, "path", "", "export to this JSON path") + return cmd +} + +func init() { + ExportCmd.AddCommand(NewExportCmd()) +} diff --git a/cmd/nfd/subcmd/export/labels.go b/cmd/nfd/subcmd/export/labels.go new file mode 100644 index 0000000000..16d36d1611 --- /dev/null +++ b/cmd/nfd/subcmd/export/labels.go @@ -0,0 +1,102 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package export + +import ( + "encoding/json" + "fmt" + "maps" + "regexp" + "slices" + "sort" + + "github.com/spf13/cobra" + "k8s.io/klog/v2" + + worker "sigs.k8s.io/node-feature-discovery/pkg/nfd-worker" + "sigs.k8s.io/node-feature-discovery/source" +) + +func NewLabelsCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "labels", + Short: "Export feature labels for given node", + RunE: func(cmd *cobra.Command, args []string) error { + + // Determine enabled feature sources + featureSources := make(map[string]source.FeatureSource) + for n, s := range source.GetAllFeatureSources() { + if ts, ok := s.(source.SupplementalSource); !ok || !ts.DisableByDefault() { + err := s.Discover() + if err != nil { + return err + } + featureSources[n] = s + } + } + featureSourceList := slices.Collect(maps.Values(featureSources)) + sort.Slice(featureSourceList, func(i, j int) bool { return featureSourceList[i].Name() < featureSourceList[j].Name() }) + + // Determine enabled label sources + labelSources := make(map[string]source.LabelSource) + for n, s := range source.GetAllLabelSources() { + if ts, ok := s.(source.SupplementalSource); !ok || !ts.DisableByDefault() { + labelSources[n] = s + } + } + labelSourcesList := slices.Collect(maps.Values(labelSources)) + sort.Slice(labelSourcesList, func(i, j int) bool { + iP, jP := labelSourcesList[i].Priority(), labelSourcesList[j].Priority() + if iP != jP { + return iP < jP + } + return labelSourcesList[i].Name() < labelSourcesList[j].Name() + }) + + labels := worker.Labels{} + labelWhiteList := *regexp.MustCompile("") + + // Get labels from all enabled label sources + for _, source := range labelSourcesList { + labelsFromSource, err := worker.GetFeatureLabels(source, labelWhiteList) + if err != nil { + klog.ErrorS(err, "discovery failed", "source", source.Name()) + continue + } + maps.Copy(labels, labelsFromSource) + } + + exportedLabels, err := json.MarshalIndent(labels, "", " ") + if err != nil { + return err + } + + if outputPath != "" { + err = writeToFile(outputPath, string(exportedLabels)) + } else { + fmt.Println(string(exportedLabels)) + } + return err + }, + } + cmd.Flags().StringVar(&outputPath, "path", "", "export to this JSON path") + return cmd +} + +func init() { + ExportCmd.AddCommand(NewLabelsCmd()) +} diff --git a/cmd/nfd/subcmd/export/utils.go b/cmd/nfd/subcmd/export/utils.go new file mode 100644 index 0000000000..a3a60e2cae --- /dev/null +++ b/cmd/nfd/subcmd/export/utils.go @@ -0,0 +1,37 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package export + +import ( + "fmt" + "os" +) + +var ( + outputPath string +) + +// writeToFile saves string content to a file at the path set by path +func writeToFile(path, content string) error { + fd, err := os.Create(path) + if err != nil { + return err + } + defer fd.Close() + _, err = fmt.Fprint(fd, content) + return err +} diff --git a/cmd/nfd/subcmd/root.go b/cmd/nfd/subcmd/root.go index d58f6b9825..e87c0f05bd 100644 --- a/cmd/nfd/subcmd/root.go +++ b/cmd/nfd/subcmd/root.go @@ -23,6 +23,7 @@ import ( "github.com/spf13/cobra" "sigs.k8s.io/node-feature-discovery/cmd/nfd/subcmd/compat" + "sigs.k8s.io/node-feature-discovery/cmd/nfd/subcmd/export" ) // RootCmd represents the base command when called without any subcommands @@ -33,6 +34,7 @@ var RootCmd = &cobra.Command{ func init() { RootCmd.AddCommand(compat.CompatCmd) + RootCmd.AddCommand(export.ExportCmd) } // Execute adds all child commands to the root command and sets flags appropriately. diff --git a/docs/usage/nfd-export.md b/docs/usage/nfd-export.md new file mode 100644 index 0000000000..54dddc3af2 --- /dev/null +++ b/docs/usage/nfd-export.md @@ -0,0 +1,58 @@ +--- +title: "Export" +layout: default +sort: 12 +--- + +# Feature Export +{: .no_toc} + +## Table of contents +{: .no_toc .text-delta} + +1. TOC +{:toc} + +--- + +## Export + +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. + +### Features + +**Feature export is in the experimental version.** + +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`: + +```bash +nfd export features +``` + +By default, JSON structure with parsed key value pairs will appear in the +terminal. To save to a file path: + +```bash +nfd export features --path features.json +``` + +### Labels + +To export equivalent labels outside of a Kubernetes context, +you can use `nfd export labels`. + +```bash +nfd export labels +``` + +Or export to an output file: + +```bash +nfd export labels --path labels.json +``` diff --git a/pkg/nfd-worker/nfd-worker-internal_test.go b/pkg/nfd-worker/nfd-worker-internal_test.go index 3aeee92f88..3cbffd5628 100644 --- a/pkg/nfd-worker/nfd-worker-internal_test.go +++ b/pkg/nfd-worker/nfd-worker-internal_test.go @@ -54,7 +54,7 @@ func TestGetLabelsWithMockSources(t *testing.T) { mockLabelSource.On("Name").Return(fakeLabelSourceName) mockLabelSource.On("GetLabels").Return(fakeFeatures, nil) - returnedLabels, err := getFeatureLabels(fakeLabelSource, labelWhiteList.Regexp) + returnedLabels, err := GetFeatureLabels(fakeLabelSource, labelWhiteList.Regexp) Convey("Proper label is returned", func() { So(returnedLabels, ShouldResemble, fakeFeatureLabels) }) @@ -67,7 +67,7 @@ func TestGetLabelsWithMockSources(t *testing.T) { expectedError := errors.New("fake error") mockLabelSource.On("GetLabels").Return(nil, expectedError) - returnedLabels, err := getFeatureLabels(fakeLabelSource, labelWhiteList.Regexp) + returnedLabels, err := GetFeatureLabels(fakeLabelSource, labelWhiteList.Regexp) Convey("No label is returned", func() { So(returnedLabels, ShouldBeNil) }) diff --git a/pkg/nfd-worker/nfd-worker.go b/pkg/nfd-worker/nfd-worker.go index f54a901ec5..3d33d578ce 100644 --- a/pkg/nfd-worker/nfd-worker.go +++ b/pkg/nfd-worker/nfd-worker.go @@ -537,7 +537,7 @@ func createFeatureLabels(sources []source.LabelSource, labelWhiteList regexp.Reg // Get labels from all enabled label sources klog.InfoS("starting feature discovery...") for _, source := range sources { - labelsFromSource, err := getFeatureLabels(source, labelWhiteList) + labelsFromSource, err := GetFeatureLabels(source, labelWhiteList) if err != nil { klog.ErrorS(err, "discovery failed", "source", source.Name()) continue @@ -555,7 +555,7 @@ func createFeatureLabels(sources []source.LabelSource, labelWhiteList regexp.Reg // getFeatureLabels returns node labels for features discovered by the // supplied source. -func getFeatureLabels(source source.LabelSource, labelWhiteList regexp.Regexp) (labels Labels, err error) { +func GetFeatureLabels(source source.LabelSource, labelWhiteList regexp.Regexp) (labels Labels, err error) { labels = Labels{} features, err := source.GetLabels() if err != nil {