Skip to content

Commit 0734b8f

Browse files
committed
kubectl-nfd: Add NodeFeatureGroup dryrun, validation and test
Signed-off-by: Oleg Zhurakivskyy <oleg.zhurakivskyy@intel.com>
1 parent 3d1b0df commit 0734b8f

10 files changed

Lines changed: 232 additions & 108 deletions

File tree

cmd/kubectl-nfd/subcmd/dryrun.go

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,34 +26,34 @@ import (
2626

2727
var dryrunCmd = &cobra.Command{
2828
Use: "dryrun",
29-
Short: "Process a NodeFeatureRule file against a NodeFeature file",
30-
Long: `Process a NodeFeatureRule file against a local NodeFeature file to dry run the rule against a node before applying it to a cluster`,
29+
Short: "Process a NodeFeatureRule or NodeFeatureGroup file against a NodeFeature file",
30+
Long: `Process a NodeFeatureRule or NodeFeatureGroup file against a local NodeFeature file to dry run the rule against a node before applying it to a cluster`,
31+
PreRunE: func(cmd *cobra.Command, args []string) error {
32+
if rule == "" {
33+
return fmt.Errorf("--rule-file must be specified")
34+
}
35+
return nil
36+
},
3137
Run: func(cmd *cobra.Command, args []string) {
32-
fmt.Printf("Evaluating NodeFeatureRule %q against NodeFeature %q\n", nodefeaturerule, nodefeature)
33-
err := kubectlnfd.DryRun(nodefeaturerule, nodefeature)
34-
if len(err) > 0 {
35-
fmt.Printf("NodeFeatureRule %q is not valid for NodeFeature %q\n", nodefeaturerule, nodefeature)
36-
for _, e := range err {
38+
fmt.Printf("Evaluating %q against NodeFeature %q\n", rule, nodefeature)
39+
errs := kubectlnfd.DryRun(rule, nodefeature)
40+
if len(errs) > 0 {
41+
fmt.Printf("%q is not valid for NodeFeature %q\n", rule, nodefeature)
42+
for _, e := range errs {
3743
cmd.PrintErrln(e)
3844
}
39-
// Return non-zero exit code to indicate failure
4045
os.Exit(1)
4146
}
42-
fmt.Printf("NodeFeatureRule %q is valid for NodeFeature %q\n", nodefeaturerule, nodefeature)
47+
fmt.Printf("%q is valid for NodeFeature %q\n", rule, nodefeature)
4348
},
4449
}
4550

4651
func init() {
4752
RootCmd.AddCommand(dryrunCmd)
4853

49-
dryrunCmd.Flags().StringVarP(&nodefeaturerule, "nodefeaturerule-file", "f", "", "Path to the NodeFeatureRule file to validate")
50-
dryrunCmd.Flags().StringVarP(&nodefeature, "nodefeature-file", "n", "", "Path to the NodeFeature file to validate against")
51-
err := dryrunCmd.MarkFlagRequired("nodefeaturerule-file")
52-
if err != nil {
53-
panic(err)
54-
}
55-
err = dryrunCmd.MarkFlagRequired("nodefeature-file")
56-
if err != nil {
54+
dryrunCmd.Flags().StringVarP(&rule, "rule-file", "f", "", "Path to the NodeFeatureRule or NodeFeatureGroup file to dry run")
55+
dryrunCmd.Flags().StringVarP(&nodefeature, "nodefeature-file", "n", "", "Path to the NodeFeature file to dry run against")
56+
if err := dryrunCmd.MarkFlagRequired("nodefeature-file"); err != nil {
5757
panic(err)
5858
}
5959
}

cmd/kubectl-nfd/subcmd/root.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ import (
2424
)
2525

2626
var (
27-
// Path to the NodeFeatureRule file to validate
28-
nodefeaturerule string
27+
// Path to the NodeFeatureRule or NodeFeatureGroup file
28+
rule string
2929
// Path to the NodeFeature file to run against the NodeFeatureRule
3030
nodefeature string
3131
// Node to validate against

cmd/kubectl-nfd/subcmd/test.go

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,32 +26,34 @@ import (
2626

2727
var testCmd = &cobra.Command{
2828
Use: "test",
29-
Short: "Test a NodeFeatureRule file against a Node",
30-
Long: `Test a NodeFeatureRule file against a Node to ensure it is valid before applying it to a cluster`,
29+
Short: "Test a NodeFeatureRule or NodeFeatureGroup file against a Node",
30+
Long: `Test a NodeFeatureRule or NodeFeatureGroup file against a Node to ensure it is valid before applying it to a cluster`,
31+
PreRunE: func(cmd *cobra.Command, args []string) error {
32+
if rule == "" {
33+
return fmt.Errorf("--rule-file must be specified")
34+
}
35+
return nil
36+
},
3137
Run: func(cmd *cobra.Command, args []string) {
32-
fmt.Printf("Evaluating NodeFeatureRule against Node %s\n", node)
33-
err := kubectlnfd.Test(nodefeaturerule, node, kubeconfig)
34-
if len(err) > 0 {
35-
fmt.Printf("NodeFeatureRule is not valid for Node %s\n", node)
36-
for _, e := range err {
38+
fmt.Printf("Evaluating %s against Node %s\n", rule, node)
39+
errs := kubectlnfd.Test(rule, node, kubeconfig)
40+
if len(errs) > 0 {
41+
fmt.Printf("%s is not valid for Node %s\n", rule, node)
42+
for _, e := range errs {
3743
cmd.PrintErrln(e)
3844
}
39-
// Return non-zero exit code to indicate failure
4045
os.Exit(1)
4146
}
42-
fmt.Printf("NodeFeatureRule is valid for Node %s\n", node)
47+
fmt.Printf("%s is valid for Node %s\n", rule, node)
4348
},
4449
}
4550

4651
func init() {
4752
RootCmd.AddCommand(testCmd)
4853

49-
testCmd.Flags().StringVarP(&nodefeaturerule, "nodefeaturerule-file", "f", "", "Path to the NodeFeatureRule file to validate")
50-
testCmd.Flags().StringVarP(&node, "nodename", "n", "", "Node to validate against")
54+
testCmd.Flags().StringVarP(&rule, "rule-file", "f", "", "Path to the NodeFeatureRule or NodeFeatureGroup file to test")
55+
testCmd.Flags().StringVarP(&node, "nodename", "n", "", "Node to test against")
5156
testCmd.Flags().StringVarP(&kubeconfig, "kubeconfig", "k", "", "kubeconfig file to use")
52-
if err := testCmd.MarkFlagRequired("nodefeaturerule-file"); err != nil {
53-
panic(err)
54-
}
5557
if err := testCmd.MarkFlagRequired("nodename"); err != nil {
5658
panic(err)
5759
}

cmd/kubectl-nfd/subcmd/validate.go

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,29 +26,30 @@ import (
2626

2727
var validateCmd = &cobra.Command{
2828
Use: "validate",
29-
Short: "Validate a NodeFeatureRule file",
30-
Long: `Validate a NodeFeatureRule file to ensure it is valid before applying it to a cluster`,
29+
Short: "Validate a NodeFeatureRule or NodeFeatureGroup file",
30+
Long: `Validate a NodeFeatureRule or NodeFeatureGroup file to ensure it is valid before applying it to a cluster`,
31+
PreRunE: func(cmd *cobra.Command, args []string) error {
32+
if rule == "" {
33+
return fmt.Errorf("--rule-file must be specified")
34+
}
35+
return nil
36+
},
3137
Run: func(cmd *cobra.Command, args []string) {
32-
fmt.Printf("Validating NodeFeatureRule %s\n", nodefeaturerule)
33-
err := kubectlnfd.ValidateNFR(nodefeaturerule)
34-
if len(err) > 0 {
35-
fmt.Printf("NodeFeatureRule %s is not valid\n", nodefeaturerule)
36-
for _, e := range err {
38+
fmt.Printf("Validating %s\n", rule)
39+
errs := kubectlnfd.Validate(rule)
40+
if len(errs) > 0 {
41+
fmt.Printf("%s is not valid\n", rule)
42+
for _, e := range errs {
3743
cmd.PrintErrln(e)
3844
}
39-
// Return non-zero exit code to indicate failure
4045
os.Exit(1)
4146
}
42-
fmt.Printf("NodeFeatureRule %s is valid\n", nodefeaturerule)
47+
fmt.Printf("%s is valid\n", rule)
4348
},
4449
}
4550

4651
func init() {
4752
RootCmd.AddCommand(validateCmd)
4853

49-
validateCmd.Flags().StringVarP(&nodefeaturerule, "nodefeaturerule-file", "f", "", "Path to the NodeFeatureRule file to validate")
50-
err := validateCmd.MarkFlagRequired("nodefeaturerule-file")
51-
if err != nil {
52-
panic(err)
53-
}
54+
validateCmd.Flags().StringVarP(&rule, "rule-file", "f", "", "Path to the NodeFeatureRule or NodeFeatureGroup file to validate")
5455
}

docs/usage/kubectl-plugin.md

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ sort: 10
2222
## Overview
2323

2424
The `kubectl` plugin `kubectl nfd` can be used to validate/dryrun and test
25-
NodeFeatureRule objects. It can be installed with the following command:
25+
NodeFeatureRule and NodeFeatureGroup objects. It can be installed with the
26+
following command:
2627

2728
```bash
2829
git clone https://github.com/kubernetes-sigs/node-feature-discovery
@@ -34,37 +35,60 @@ mv ./bin/kubectl-nfd ${KUBECTL_PATH}
3435

3536
### Validate
3637

37-
The plugin can be used to validate a NodeFeatureRule object:
38+
The plugin can be used to validate a NodeFeatureRule or NodeFeatureGroup object.
39+
The kind is detected automatically from the file content:
3840

3941
```bash
40-
kubectl nfd validate -f <nodefeaturerule.yaml>
42+
kubectl nfd validate -f <nodefeaturerule-or-nodefeaturegroup.yaml>
43+
```
44+
45+
You can use the example files to try it out:
46+
47+
```bash
48+
$ kubectl nfd validate -f examples/nodefeaturegroup.yaml
49+
Validating examples/nodefeaturegroup.yaml
50+
Validating rule: kernel version
51+
Validating rule: veth kernel module
52+
examples/nodefeaturegroup.yaml is valid
4153
```
4254

4355
### Test
4456

45-
The plugin can be used to test a NodeFeatureRule object against a node:
57+
The plugin can be used to test a NodeFeatureRule or NodeFeatureGroup object
58+
against a node. The kind is detected automatically from the file content:
4659

4760
```bash
48-
kubectl nfd test -f <nodefeaturerule.yaml> -n <node-name>
61+
kubectl nfd test -f <nodefeaturerule-or-nodefeaturegroup.yaml> -n <node-name>
4962
```
5063

5164
### DryRun
5265

53-
The plugin can be used to DryRun a NodeFeatureRule object against a NodeFeature
54-
file:
66+
The plugin can be used to dry run a NodeFeatureRule or NodeFeatureGroup object
67+
against a NodeFeature file. The kind is detected automatically from the file
68+
content:
5569

5670
```bash
5771
kubectl get -n node-feature-discovery nodefeature <nodename> -o yaml > <nodefeature.yaml>
58-
kubectl nfd dryrun -f <nodefeaturerule.yaml> -n <nodefeature.yaml>
72+
kubectl nfd dryrun -f <nodefeaturerule-or-nodefeaturegroup.yaml> -n <nodefeature.yaml>
5973
```
6074

61-
Or you can use the example NodeFeature file(it is a minimal NodeFeature file):
75+
For example, using the example files:
6276

6377
```bash
6478
$ kubectl nfd dryrun -f examples/nodefeaturerule.yaml -n examples/nodefeature.yaml
65-
Evaluating NodeFeatureRule "examples/nodefeaturerule.yaml" against NodeFeature "examples/nodefeature.yaml"
79+
Evaluating "examples/nodefeaturerule.yaml" against NodeFeature "examples/nodefeature.yaml"
6680
Processing rule: my sample rule
6781
*** Labels ***
6882
vendor.io/my-sample-feature=true
69-
NodeFeatureRule "examples/nodefeaturerule.yaml" is valid for NodeFeature "examples/nodefeature.yaml"
83+
"examples/nodefeaturerule.yaml" is valid for NodeFeature "examples/nodefeature.yaml"
84+
```
85+
86+
```bash
87+
$ kubectl nfd dryrun -f examples/nodefeaturegroup.yaml -n examples/nodefeature.yaml
88+
Evaluating "examples/nodefeaturegroup.yaml" against NodeFeature "examples/nodefeature.yaml"
89+
Processing rule: kernel version
90+
Rule "kernel version" did not match
91+
Processing rule: veth kernel module
92+
Rule "veth kernel module" did not match
93+
"examples/nodefeaturegroup.yaml" is valid for NodeFeature "examples/nodefeature.yaml"
7094
```

examples/nodefeaturegroup.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,8 @@ spec:
99
- feature: kernel.version
1010
matchExpressions:
1111
major: {op: In, value: ["6"]}
12+
- name: "veth kernel module"
13+
matchFeatures:
14+
- feature: kernel.loadedmodule
15+
matchExpressions:
16+
veth: {op: Exists}

pkg/kubectl-nfd/common.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
Copyright 2026 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 kubectlnfd
18+
19+
import (
20+
"fmt"
21+
"os"
22+
23+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
24+
"sigs.k8s.io/yaml"
25+
26+
nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/api/nfd/v1alpha1"
27+
)
28+
29+
func parseRuleFile(filepath string) (obj interface{}) {
30+
file, err := os.ReadFile(filepath)
31+
if err != nil {
32+
return []error{fmt.Errorf("error reading file: %w", err)}
33+
}
34+
typeMeta := metav1.TypeMeta{}
35+
if err = yaml.Unmarshal(file, &typeMeta); err != nil {
36+
return []error{fmt.Errorf("error reading resource kind: %w", err)}
37+
}
38+
switch typeMeta.Kind {
39+
case "NodeFeatureRule":
40+
nfr := nfdv1alpha1.NodeFeatureRule{}
41+
if err := yaml.Unmarshal(file, &nfr); err != nil {
42+
return []error{fmt.Errorf("error reading NodeFeatureRule file: %w", err)}
43+
}
44+
return &nfr
45+
case "NodeFeatureGroup":
46+
nfg := nfdv1alpha1.NodeFeatureGroup{}
47+
if err := yaml.Unmarshal(file, &nfg); err != nil {
48+
return []error{fmt.Errorf("error reading NodeFeatureGroup file: %w", err)}
49+
}
50+
return &nfg
51+
default:
52+
return []error{fmt.Errorf("unsupported resource kind %q: must be NodeFeatureRule or NodeFeatureGroup", typeMeta.Kind)}
53+
}
54+
}

pkg/kubectl-nfd/dryrun.go

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -31,34 +31,51 @@ import (
3131
"sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/validate"
3232
)
3333

34-
func DryRun(nodefeaturerulepath, nodefeaturepath string) []error {
34+
func processNodeFeatureGroup(nodeFeatureGroup nfdv1alpha1.NodeFeatureGroup, nodeFeature nfdv1alpha1.NodeFeatureSpec) []error {
3535
var errs []error
36-
nfr := nfdv1alpha1.NodeFeatureRule{}
37-
nf := nfdv1alpha1.NodeFeature{}
3836

39-
nfrFile, err := os.ReadFile(nodefeaturerulepath)
40-
if err != nil {
41-
return []error{fmt.Errorf("error reading NodeFeatureRule file: %w", err)}
37+
for _, rule := range nodeFeatureGroup.Spec.Rules {
38+
fmt.Println("Processing rule: ", rule.Name)
39+
ruleOut, err := nodefeaturerule.ExecuteGroupRule(&rule, &nodeFeature.Features, true)
40+
if err != nil {
41+
errs = append(errs, fmt.Errorf("failed to process rule %q: %w", rule.Name, err))
42+
continue
43+
}
44+
if ruleOut.MatchStatus == nil || !ruleOut.MatchStatus.IsMatch {
45+
fmt.Printf("Rule %q did not match\n", rule.Name)
46+
continue
47+
}
48+
fmt.Printf("Rule %q matched\n", rule.Name)
49+
if len(ruleOut.Vars) > 0 {
50+
fmt.Println("***\tVars\t***")
51+
for k, v := range ruleOut.Vars {
52+
fmt.Printf("%s=%s\n", k, v)
53+
}
54+
}
4255
}
4356

44-
err = yaml.Unmarshal(nfrFile, &nfr)
45-
if err != nil {
46-
return []error{fmt.Errorf("error parsing NodeFeatureRule: %w", err)}
47-
}
57+
return errs
58+
}
4859

60+
func DryRun(resourcepath, nodefeaturepath string) []error {
4961
nfFile, err := os.ReadFile(nodefeaturepath)
5062
if err != nil {
51-
return []error{fmt.Errorf("error reading NodeFeatureRule file: %w", err)}
63+
return []error{fmt.Errorf("error reading NodeFeature file: %w", err)}
5264
}
53-
54-
err = yaml.Unmarshal(nfFile, &nf)
55-
if err != nil {
56-
return []error{fmt.Errorf("error parsing NodeFeatureRule: %w", err)}
65+
nf := nfdv1alpha1.NodeFeature{}
66+
if err = yaml.Unmarshal(nfFile, &nf); err != nil {
67+
return []error{fmt.Errorf("error parsing NodeFeature: %w", err)}
5768
}
5869

59-
errs = append(errs, processNodeFeatureRule(nfr, nf.Spec)...)
60-
61-
return errs
70+
t := parseRuleFile(resourcepath)
71+
switch o := t.(type) {
72+
case *nfdv1alpha1.NodeFeatureRule:
73+
return processNodeFeatureRule(*o, nf.Spec)
74+
case *nfdv1alpha1.NodeFeatureGroup:
75+
return processNodeFeatureGroup(*o, nf.Spec)
76+
default:
77+
return []error{fmt.Errorf("unsupported resource %v: must be NodeFeatureRule or NodeFeatureGroup", t)}
78+
}
6279
}
6380

6481
func processNodeFeatureRule(nodeFeatureRule nfdv1alpha1.NodeFeatureRule, nodeFeature nfdv1alpha1.NodeFeatureSpec) []error {

0 commit comments

Comments
 (0)