1
0
Fork 0
mirror of https://github.com/kubernetes-sigs/node-feature-discovery.git synced 2024-12-14 11:57:51 +00:00

Merge pull request #1446 from ArangoGutierrez/nfd.cli

Add Kubectl NFD plugin
This commit is contained in:
Kubernetes Prow Robot 2023-12-21 16:41:51 +01:00 committed by GitHub
commit 0ca16c01e8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 981 additions and 11 deletions

View file

@ -1,4 +1,4 @@
.PHONY: all test templates yamls
.PHONY: all test templates yamls build build-%
.FORCE:
GO_CMD ?= go
@ -90,12 +90,17 @@ IMAGE_BUILD_ARGS_MINIMAL = --target minimal \
all: image
build:
@mkdir -p bin
$(GO_CMD) build -v -o bin $(BUILD_FLAGS) ./cmd/...
BUILD_BINARIES := nfd-master nfd-worker nfd-topology-updater nfd-gc kubectl-nfd
install:
$(GO_CMD) install -v $(BUILD_FLAGS) ./cmd/...
build-%:
$(GO_CMD) build -v -o bin $(BUILD_FLAGS) ./cmd/$*
build: $(foreach bin, $(BUILD_BINARIES), build-$(bin))
install-%:
$(GO_CMD) install -v $(BUILD_FLAGS) ./cmd/$*
install: $(foreach bin, $(BUILD_BINARIES), install-$(bin))
image: yamls
$(IMAGE_BUILD_CMD) $(IMAGE_BUILD_ARGS) $(IMAGE_BUILD_ARGS_FULL)

28
cmd/kubectl-nfd/main.go Normal file
View file

@ -0,0 +1,28 @@
/*
Copyright 2023 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 main
import "sigs.k8s.io/node-feature-discovery/cmd/kubectl-nfd/subcmd"
const (
// ProgramName is the canonical name of this program
ProgramName = "kubectl-nfd"
)
func main() {
subcmd.Execute()
}

View file

@ -0,0 +1,59 @@
/*
Copyright 2023 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 subcmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
kubectlnfd "sigs.k8s.io/node-feature-discovery/pkg/kubectl-nfd"
)
var dryrunCmd = &cobra.Command{
Use: "dryrun",
Short: "Process a NodeFeatureRule file against a NodeFeature file",
Long: `Process a NodeFeatureRule file against a local NodeFeature file to dry run the rule against a node before applying it to a cluster`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("Evaluating NodeFeatureRule %q against NodeFeature %q\n", nodefeaturerule, nodefeature)
err := kubectlnfd.DryRun(nodefeaturerule, nodefeature)
if len(err) > 0 {
fmt.Printf("NodeFeatureRule %q is not valid for NodeFeature %q\n", nodefeaturerule, nodefeature)
for _, e := range err {
cmd.PrintErrln(e)
}
// Return non-zero exit code to indicate failure
os.Exit(1)
}
fmt.Printf("NodeFeatureRule %q is valid for NodeFeature %q\n", nodefeaturerule, nodefeature)
},
}
func init() {
RootCmd.AddCommand(dryrunCmd)
dryrunCmd.Flags().StringVarP(&nodefeaturerule, "nodefeaturerule-file", "f", "", "Path to the NodeFeatureRule file to validate")
dryrunCmd.Flags().StringVarP(&nodefeature, "nodefeature-file", "n", "", "Path to the NodeFeature file to validate against")
err := dryrunCmd.MarkFlagRequired("nodefeaturerule-file")
if err != nil {
panic(err)
}
err = dryrunCmd.MarkFlagRequired("nodefeature-file")
if err != nil {
panic(err)
}
}

View file

@ -0,0 +1,54 @@
/*
Copyright 2023 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 subcmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var (
// Path to the NodeFeatureRule file to validate
nodefeaturerule string
// Path to the NodeFeature file to run against the NodeFeatureRule
nodefeature string
// Node to validate against
node string
// kubeconfig file to use
kubeconfig string
)
// RootCmd represents the base command when called without any subcommands
var RootCmd = &cobra.Command{
Use: "kubectl-nfd",
Short: "NFD kubectl plugin",
Long: `kubectl plugin for NFD
Debug tool to validate/dryrun/test NodeFeatureRules
for more information see:
https://kubernetes-sigs.github.io/node-feature-discovery/v0.14/usage/customization-guide.html#nodefeaturerule-custom-resource`,
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
if err := RootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

View file

@ -0,0 +1,56 @@
/*
Copyright 2023 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 subcmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
kubectlnfd "sigs.k8s.io/node-feature-discovery/pkg/kubectl-nfd"
)
var testCmd = &cobra.Command{
Use: "test",
Short: "Test a NodeFeatureRule file against a Node",
Long: `Test a NodeFeatureRule file against a Node to ensure it is valid before applying it to a cluster`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("Evaluating NodeFeatureRule against Node %s\n", node)
err := kubectlnfd.Test(nodefeaturerule, node, kubeconfig)
if len(err) > 0 {
fmt.Printf("NodeFeatureRule is not valid for Node %s\n", node)
for _, e := range err {
cmd.PrintErrln(e)
}
// Return non-zero exit code to indicate failure
os.Exit(1)
}
fmt.Printf("NodeFeatureRule is valid for Node %s\n", node)
},
}
func init() {
RootCmd.AddCommand(testCmd)
testCmd.Flags().StringVarP(&nodefeaturerule, "nodefeaturerule-file", "f", "", "Path to the NodeFeatureRule file to validate")
testCmd.Flags().StringVarP(&node, "nodename", "n", "", "Node to validate against")
testCmd.Flags().StringVarP(&kubeconfig, "kubeconfig", "k", "", "kubeconfig file to use")
err := testCmd.MarkFlagRequired("nodefeaturerule-file")
if err != nil {
panic(err)
}
}

View file

@ -0,0 +1,54 @@
/*
Copyright 2023 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 subcmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
kubectlnfd "sigs.k8s.io/node-feature-discovery/pkg/kubectl-nfd"
)
var validateCmd = &cobra.Command{
Use: "validate",
Short: "Validate a NodeFeatureRule file",
Long: `Validate a NodeFeatureRule file to ensure it is valid before applying it to a cluster`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("Validating NodeFeatureRule %s\n", nodefeaturerule)
err := kubectlnfd.ValidateNFR(nodefeaturerule)
if len(err) > 0 {
fmt.Printf("NodeFeatureRule %s is not valid\n", nodefeaturerule)
for _, e := range err {
cmd.PrintErrln(e)
}
// Return non-zero exit code to indicate failure
os.Exit(1)
}
fmt.Printf("NodeFeatureRule %s is valid\n", nodefeaturerule)
},
}
func init() {
RootCmd.AddCommand(validateCmd)
validateCmd.Flags().StringVarP(&nodefeaturerule, "nodefeaturerule-file", "f", "", "Path to the NodeFeatureRule file to validate")
err := validateCmd.MarkFlagRequired("nodefeaturerule-file")
if err != nil {
panic(err)
}
}

View file

@ -0,0 +1,68 @@
---
title: "Kubectl plugin cmdline reference"
layout: default
sort: 8
---
# Commandline flags of kubectl-nfd (plugin)
{: .no_toc}
## Table of contents
{: .no_toc .text-delta}
1. TOC
{:toc}
---
To quickly view available command line flags execute `kubectl nfd -help`.
### -h, -help
Print usage and exit.
## Validate
Validate a NodeFeatureRule file.
### -f / --nodefeature-file
The `--nodefeature-file` flag specifies the path to the NodeFeatureRule file
to validate.
## Test
Test a NodeFeatureRule file against a node without applying it.
### -k, --kubeconfig
The `--kubeconfig` flag specifies the path to the kubeconfig file to use for
CLI requests.
### -s, --namespace
The `--namespace` flag specifies the namespace to use for CLI requests.
Default: `default`.
### -n, --nodename
The `--nodename` flag specifies the name of the node to test the
NodeFeatureRule against.
### -f, --nodefeaturerule-file
The `--nodefeaturerule-file` flag specifies the path to the NodeFeatureRule file
to test.
## DryRun
Process a NodeFeatureRule file against a NodeFeature file.
### -f, --nodefeaturerule-file
The `--nodefeaturerule-file` flag specifies the path to the NodeFeatureRule file
to test.
### -n, --nodefeature-file
The `--nodefeature-file` flag specifies the path to the NodeFeature file to test.

View file

@ -1,7 +1,7 @@
---
title: "Versions"
layout: default
sort: 8
sort: 9
---
# Versions and deprecation

View file

@ -0,0 +1,70 @@
---
title: "Kubectl plugin"
layout: default
sort: 10
---
# Kubectl plugin
{: .no_toc}
## Table of contents
{: .no_toc .text-delta}
1. TOC
{:toc}
---
> ***Developer Preview*** This feature is currently in developer preview and
> subject to change. It is not recommended to use it in production
> environments.
## Overview
The `kubectl` plugin `kubectl nfd` can be used to validate/dryrun and test
NodeFeatureRule objects. It can be installed with the following command:
```bash
git clone https://github.com/kubernetes-sigs/node-feature-discovery
cd node-feature-discovery
make build-kubectl-nfd
KUBECTL_PATH=/usr/local/bin/
mv ./bin/kubectl-nfd ${KUBECTL_PATH}
```
### Validate
The plugin can be used to validate a NodeFeatureRule object:
```bash
kubectl nfd validate -f <nodefeaturerule.yaml>
```
### Test
The plugin can be used to test a NodeFeatureRule object against a node:
```bash
kubectl nfd test -f <nodefeaturerule.yaml> -n <node-name>
```
### DryRun
The plugin can be used to DryRun a NodeFeatureRule object against a NodeFeature
file:
```bash
kubectl get -n node-feature-discovery nodefeature <nodename> -o yaml > <nodefeature.yaml>
kubectl nfd dryrun -f <nodefeaturerule.yaml> -n <nodefeature.yaml>
```
Or you can use the example NodeFeature file(it is a minimal NodeFeature file):
```bash
$ kubectl nfd dryrun -f examples/nodefeaturerule.yaml -n examples/nodefeature.yaml
Processing rule: my sample rule
*** Labels ***
vendor.io/my-sample-feature=true
Evaluating NodeFeatureRule "examples/nodefeaturerule.yaml" against NodeFeature "examples/nodefeature.yaml"
NodeFeatureRule "examples/nodefeaturerule.yaml" is valid for NodeFeature "examples/nodefeature.yaml"
```

119
examples/nodefeature.yaml Normal file
View file

@ -0,0 +1,119 @@
---
# Example NodeFeature object
apiVersion: nfd.k8s-sigs.io/v1alpha1
kind: NodeFeature
metadata:
labels:
nfd.node.kubernetes.io/node-name: example-node
name: example-node
namespace: node-feature-discovery
spec:
features:
attributes:
cpu.coprocessor:
elements: {}
cpu.cstate:
elements: {}
cpu.model:
elements:
family: "0"
id: "0"
vendor_id: VendorUnknown
cpu.pstate:
elements: {}
cpu.rdt:
elements: {}
cpu.security:
elements: {}
cpu.sst:
elements: {}
cpu.topology:
elements:
hardware_multithreading: "false"
kernel.config:
elements:
DUMMY: m
NET: "y"
X86: "y"
kernel.selinux:
elements:
enabled: "false"
kernel.version:
elements:
full: 6.3.13-linuxkit
major: "6"
minor: "3"
revision: "13"
local.label:
elements: {}
system.name:
elements:
nodename: example-node
system.osrelease:
elements:
BUG_REPORT_URL: https://bugs.launchpad.net/ubuntu/
HOME_URL: https://www.ubuntu.com/
ID: ubuntu
ID_LIKE: debian
NAME: Ubuntu
PRETTY_NAME: Ubuntu 22.04.2 LTS
PRIVACY_POLICY_URL: https://www.ubuntu.com/legal/terms-and-policies/privacy-policy
SUPPORT_URL: https://help.ubuntu.com/
UBUNTU_CODENAME: jammy
VERSION: 22.04.2 LTS (Jammy Jellyfish)
VERSION_CODENAME: jammy
VERSION_ID: "22.04"
VERSION_ID.major: "22"
VERSION_ID.minor: "04"
flags:
cpu.cpuid:
elements:
SHA1: {}
SHA2: {}
SHA3: {}
SHA512: {}
kernel.loadedmodule:
elements:
auth_rpcgss: {}
dummy: {}
fakeowner: {}
grace: {}
grpcfuse: {}
iscsi_tcp: {}
libiscsi: {}
libiscsi_tcp: {}
lockd: {}
nfs: {}
nfsd: {}
scsi_transport_iscsi: {}
shiftfs: {}
sunrpc: {}
vmw_vsock_virtio_transport: {}
vmw_vsock_virtio_transport_common: {}
vsock: {}
xfrm_algo: {}
xfrm_user: {}
instances:
memory.nv:
elements: []
network.device:
elements: []
usb.device:
elements: []
labels:
cpu-hardware_multithreading: "false"
cpu-model.family: "0"
cpu-model.id: "0"
cpu-model.vendor_id: VendorUnknown
kernel-config.NO_HZ: "true"
kernel-config.NO_HZ_IDLE: "true"
kernel-config.PREEMPT: "true"
kernel-version.full: 6.3.13-linuxkit
kernel-version.major: "6"
kernel-version.minor: "3"
kernel-version.revision: "13"
storage-nonrotationaldisk: "true"
system-os_release.ID: ubuntu
system-os_release.VERSION_ID: "22.04"
system-os_release.VERSION_ID.major: "22"
system-os_release.VERSION_ID.minor: "04"

View file

@ -10,10 +10,10 @@ spec:
matchFeatures:
- feature: kernel.loadedmodule
matchExpressions:
dummy: {op: Exists}
dummy: { op: Exists }
- feature: kernel.config
matchExpressions:
X86: {op: In, value: ["y"]}
X86: { op: In, value: ["y"] }
---
apiVersion: nfd.k8s-sigs.io/v1alpha1
kind: NodeFeatureRule
@ -28,4 +28,4 @@ spec:
matchFeatures:
- feature: kernel.version
matchExpressions:
major: {op: Exists}
major: { op: Exists }

2
go.mod
View file

@ -18,6 +18,7 @@ require (
github.com/prometheus/client_golang v1.16.0
github.com/smartystreets/assertions v1.2.0
github.com/smartystreets/goconvey v1.6.4
github.com/spf13/cobra v1.7.0
github.com/stretchr/testify v1.8.4
github.com/vektra/errors v0.0.0-20140903201135-c64d83aba85a
golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb
@ -137,7 +138,6 @@ require (
github.com/rubiojr/go-vhd v0.0.0-20200706105327-02e210299021 // indirect
github.com/seccomp/libseccomp-golang v0.10.0 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/spf13/cobra v1.7.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stoewer/go-strcase v1.2.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect

View file

@ -19,6 +19,7 @@ package validate
import (
"fmt"
"strings"
"text/template"
corev1 "k8s.io/api/core/v1"
k8sQuantity "k8s.io/apimachinery/pkg/api/resource"
@ -38,6 +39,58 @@ var (
ErrEmptyTaintEffect = fmt.Errorf("empty taint effect")
)
// MatchAny validates a slice of MatchAnyElem and returns a slice of errors if
// any of the MatchAnyElem are invalid.
func MatchAny(matchAny []nfdv1alpha1.MatchAnyElem) []error {
var validationErr []error
for _, matcher := range matchAny {
validationErr = append(validationErr, MatchFeatures(matcher.MatchFeatures)...)
}
return validationErr
}
// MatchFeatures validates a slice of FeatureMatcher and returns a slice of
// errors if any of the FeatureMatcher are invalid.
func MatchFeatures(matchFeature nfdv1alpha1.FeatureMatcher) []error {
var validationErr []error
for _, match := range matchFeature {
nameSplit := strings.SplitN(match.Feature, ".", 2)
if len(nameSplit) != 2 {
validationErr = append(validationErr, fmt.Errorf("invalid feature name %v (not <domain>.<feature>), cannot be used for templating", match.Feature))
}
}
return validationErr
}
// Template validates a template string and returns a slice of errors if the
// template is invalid.
func Template(labelsTemplate string) []error {
var validationErr []error
// Validate template
_, err := template.New("").Option("missingkey=error").Parse(labelsTemplate)
if err != nil {
validationErr = append(validationErr, fmt.Errorf("invalid template: %w", err))
}
return validationErr
}
// Labels validates a map of labels and returns a slice of errors if any of the
// labels are invalid.
func Labels(labels map[string]string) []error {
var errs []error
for key, value := range labels {
if err := Label(key, value); err != nil {
errs = append(errs, fmt.Errorf("invalid label %q:%q %w", key, value, err))
}
}
return errs
}
// Label validates a label key and value and returns an error if the key or
// value is invalid.
func Label(key, value string) error {
@ -68,6 +121,18 @@ func Label(key, value string) error {
return nil
}
// Annotations validates a map of annotations and returns a slice of errors if
// any of the annotations are invalid.
func Annotations(annotations map[string]string) []error {
var errs []error
for key, value := range annotations {
if err := Annotation(key, value); err != nil {
errs = append(errs, fmt.Errorf("invalid annotation %q:%q %w", key, value, err))
}
}
return errs
}
// Annotation validates an annotation key and value and returns an error if the
// key or value is invalid.
func Annotation(key, value string) error {
@ -97,6 +162,18 @@ func Annotation(key, value string) error {
return nil
}
// Taints validates a slice of taints and returns a slice of errors if any of
// the taints are invalid.
func Taints(taints []corev1.Taint) []error {
var errs []error
for _, taint := range taints {
if err := Taint(&taint); err != nil {
errs = append(errs, fmt.Errorf("invalid taint %s=%s:%s: %w", taint.Key, taint.Value, taint.Effect, err))
}
}
return errs
}
// Taint validates a taint key and value and returns an error if the key or
// value is invalid.
func Taint(taint *corev1.Taint) error {
@ -127,6 +204,18 @@ func Taint(taint *corev1.Taint) error {
return nil
}
// ExtendedResources validates a map of extended resources and returns a slice
// of errors if any of the extended resources are invalid.
func ExtendedResources(extendedResources map[string]string) []error {
var errs []error
for key, value := range extendedResources {
if err := ExtendedResource(key, value); err != nil {
errs = append(errs, fmt.Errorf("invalid extended resource %q:%q %w", key, value, err))
}
}
return errs
}
// ExtendedResource validates an extended resource key and value and returns an
// error if the key or value is invalid.
func ExtendedResource(key, value string) error {

195
pkg/kubectl-nfd/dryrun.go Normal file
View file

@ -0,0 +1,195 @@
/*
Copyright 2023 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 kubectlnfd
import (
"fmt"
"os"
"strings"
"sigs.k8s.io/yaml"
corev1 "k8s.io/api/core/v1"
nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
"sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1/nodefeaturerule"
"sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/validate"
)
func DryRun(nodefeaturerulepath, nodefeaturepath string) []error {
var errs []error
nfr := nfdv1alpha1.NodeFeatureRule{}
nf := nfdv1alpha1.NodeFeature{}
nfrFile, err := os.ReadFile(nodefeaturerulepath)
if err != nil {
return []error{fmt.Errorf("error reading NodeFeatureRule file: %w", err)}
}
err = yaml.Unmarshal(nfrFile, &nfr)
if err != nil {
return []error{fmt.Errorf("error parsing NodeFeatureRule: %w", err)}
}
nfFile, err := os.ReadFile(nodefeaturepath)
if err != nil {
return []error{fmt.Errorf("error reading NodeFeatureRule file: %w", err)}
}
err = yaml.Unmarshal(nfFile, &nf)
if err != nil {
return []error{fmt.Errorf("error parsing NodeFeatureRule: %w", err)}
}
errs = append(errs, processNodeFeatureRule(nfr, nf.Spec)...)
return errs
}
func processNodeFeatureRule(nodeFeatureRule nfdv1alpha1.NodeFeatureRule, nodeFeature nfdv1alpha1.NodeFeatureSpec) []error {
var errs []error
var taints []corev1.Taint
extendedResources := make(map[string]string)
labels := make(map[string]string)
annotations := make(map[string]string)
for _, rule := range nodeFeatureRule.Spec.Rules {
fmt.Println("Processing rule: ", rule.Name)
ruleOut, err := nodefeaturerule.Execute(&rule, &nodeFeature.Features)
if err != nil {
errs = append(errs, fmt.Errorf("failed to process rule: %q - %w", rule.Name, err))
continue
}
// taints
taints = append(taints, ruleOut.Taints...)
// labels
for k, v := range ruleOut.Labels {
// Dynamic Value
if strings.HasPrefix(v, "@") {
dvalue, err := getDynamicValue(v, &nodeFeature.Features)
if err != nil {
errs = append(errs, fmt.Errorf("failed to get dynamic value for label %q: %w", k, err))
continue
}
labels[k] = dvalue
continue
}
labels[k] = v
}
// extended resources
for k, v := range ruleOut.ExtendedResources {
// Dynamic Value
if strings.HasPrefix(v, "@") {
dvalue, err := getDynamicValue(v, &nodeFeature.Features)
if err != nil {
errs = append(errs, fmt.Errorf("failed to get dynamic value for extendedResource %q: %w", k, err))
continue
}
extendedResources[k] = dvalue
continue
}
extendedResources[k] = v
}
// annotations
for k, v := range ruleOut.Annotations {
annotations[k] = v
}
}
if len(taints) > 0 {
taintValidation := validate.Taints(taints)
fmt.Println("***\tTaints\t***")
for _, taint := range taints {
fmt.Println(taint)
}
if len(taintValidation) > 0 {
fmt.Println("\t-Validation errors-")
for _, err := range taintValidation {
fmt.Println(err)
}
}
}
if len(labels) > 0 {
labelValidation := validate.Labels(labels)
fmt.Println("***\tLabels\t***")
for k, v := range labels {
fmt.Printf("%s=%s\n", k, v)
}
if len(labelValidation) > 0 {
fmt.Println("\t-Validation errors-")
for _, err := range labelValidation {
fmt.Println(err)
}
}
}
if len(extendedResources) > 0 {
resourceValidation := processExtendedResources(extendedResources, nodeFeature)
fmt.Println("***\tExtended Resources\t***")
for k, v := range extendedResources {
fmt.Printf("%s=%s\n", k, v)
}
if len(resourceValidation) > 0 {
fmt.Println("\t-Validation errors-")
for _, err := range resourceValidation {
fmt.Println(err)
}
}
}
if len(annotations) > 0 {
annotationsValidation := validate.Annotations(annotations)
fmt.Println("***\tAnnotations\t***")
for k, v := range annotations {
fmt.Printf("%s=%s\n", k, v)
}
if len(annotationsValidation) > 0 {
fmt.Println("\t-Validation errors-")
for _, err := range annotationsValidation {
fmt.Println(err)
}
}
}
return errs
}
func processExtendedResources(extendedResources map[string]string, nodeFeature nfdv1alpha1.NodeFeatureSpec) []error {
var errs []error
return append(errs, validate.ExtendedResources(extendedResources)...)
}
func getDynamicValue(value string, features *nfdv1alpha1.Features) (string, error) {
// value is a string in the form of attribute.featureset.elements
split := strings.SplitN(value[1:], ".", 3)
if len(split) != 3 {
return "", fmt.Errorf("value %s is not in the form of '@domain.feature.element'", value)
}
featureName := split[0] + "." + split[1]
elementName := split[2]
attrFeatureSet, ok := features.Attributes[featureName]
if !ok {
return "", fmt.Errorf("feature %s not found", featureName)
}
element, ok := attrFeatureSet.Elements[elementName]
if !ok {
return "", fmt.Errorf("element %s not found on feature %s", elementName, featureName)
}
return element, nil
}

79
pkg/kubectl-nfd/test.go Normal file
View file

@ -0,0 +1,79 @@
/*
Copyright 2023 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 kubectlnfd
import (
"fmt"
"os"
"time"
k8sLabels "k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/tools/clientcmd"
nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
nfdclientset "sigs.k8s.io/node-feature-discovery/pkg/generated/clientset/versioned"
nfdinformers "sigs.k8s.io/node-feature-discovery/pkg/generated/informers/externalversions"
"sigs.k8s.io/yaml"
)
func Test(nodefeaturerulepath, nodeName, kubeconfig string) []error {
var errs []error
var err error
nfr := nfdv1alpha1.NodeFeatureRule{}
if kubeconfig == "" {
kubeconfig = os.Getenv("KUBECONFIG")
}
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
return []error{fmt.Errorf("error building kubeconfig: %w", err)}
}
nfdClient := nfdclientset.NewForConfigOrDie(config)
informerFactory := nfdinformers.NewSharedInformerFactory(nfdClient, 1*time.Second)
featureLister := informerFactory.Nfd().V1alpha1().NodeFeatures().Lister()
sel := k8sLabels.SelectorFromSet(k8sLabels.Set{nfdv1alpha1.NodeFeatureObjNodeNameLabel: nodeName})
objs, err := featureLister.List(sel)
if err != nil {
return []error{fmt.Errorf("failed to get NodeFeature resources for node %q: %w", nodeName, err)}
}
features := nfdv1alpha1.NewNodeFeatureSpec()
if len(objs) > 0 {
features = objs[0].Spec.DeepCopy()
for _, o := range objs[1:] {
s := o.Spec.DeepCopy()
s.MergeInto(features)
}
}
nfrFile, err := os.ReadFile(nodefeaturerulepath)
if err != nil {
return []error{fmt.Errorf("error reading NodeFeatureRule file: %w", err)}
}
err = yaml.Unmarshal(nfrFile, &nfr)
if err != nil {
return []error{fmt.Errorf("error parsing NodeFeatureRule: %w", err)}
}
errs = append(errs, processNodeFeatureRule(nfr, *features)...)
return errs
}

View file

@ -0,0 +1,94 @@
/*
Copyright 2023 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 kubectlnfd
import (
"fmt"
"os"
"strings"
"k8s.io/apimachinery/pkg/api/resource"
"sigs.k8s.io/yaml"
nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
"sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/validate"
)
// Given a file path, read the file and check if is a valid NodeFeatureRule file
func ValidateNFR(filepath string) []error {
var err error
var validationErr []error
file, err := os.ReadFile(filepath)
if err != nil {
return []error{fmt.Errorf("error reading NodeFeatureRule file: %w", err)}
}
nfr := nfdv1alpha1.NodeFeatureRule{}
err = yaml.Unmarshal(file, &nfr)
if err != nil {
return []error{fmt.Errorf("error reading NodeFeatureRule file: %w", err)}
}
for _, rule := range nfr.Spec.Rules {
fmt.Println("Validating rule: ", rule.Name)
// Validate Rule Name
if rule.Name == "" {
validationErr = append(validationErr, fmt.Errorf("rule name cannot be empty"))
}
// Validate Annotations
validationErr = append(validationErr, validate.Annotations(rule.Annotations)...)
// Validate labels
// Dummy dynamic values before validating labels
labels := rule.Labels
for k, v := range labels {
if strings.HasPrefix(v, "@") {
labels[k] = resource.NewQuantity(0, resource.DecimalSI).String()
}
}
validationErr = append(validationErr, validate.Labels(labels)...)
// Validate Taints
validationErr = append(validationErr, validate.Taints(rule.Taints)...)
// Validate extended Resources
// Dummy dynamic values before validating extended resources
extendedResources := rule.ExtendedResources
for k, v := range extendedResources {
if strings.HasPrefix(v, "@") {
extendedResources[k] = resource.NewQuantity(0, resource.DecimalSI).String()
}
}
validationErr = append(validationErr, validate.ExtendedResources(extendedResources)...)
// Validate LabelsTemplate
validationErr = append(validationErr, validate.Template(rule.LabelsTemplate)...)
// Validate VarsTemplate
validationErr = append(validationErr, validate.Template(rule.VarsTemplate)...)
// Validate matchFeatures
validationErr = append(validationErr, validate.MatchFeatures(rule.MatchFeatures)...)
// Validate matchAny
validationErr = append(validationErr, validate.MatchAny(rule.MatchAny)...)
}
return validationErr
}