From 57b6035b718cd55eb279b644c7db3f4abe2207ea Mon Sep 17 00:00:00 2001 From: Carlos Eduardo Arango Gutierrez Date: Wed, 1 Nov 2023 15:55:56 +0100 Subject: [PATCH] Add kubectl-nfd kubectl-nfd is a kubectl plugin for debbuging NodeFeatureRules Signed-off-by: Carlos Eduardo Arango Gutierrez --- Makefile | 17 +- cmd/kubectl-nfd/main.go | 28 +++ cmd/kubectl-nfd/subcmd/dryrun.go | 59 ++++++ cmd/kubectl-nfd/subcmd/root.go | 54 +++++ cmd/kubectl-nfd/subcmd/test.go | 56 +++++ cmd/kubectl-nfd/subcmd/validate.go | 54 +++++ .../reference/plugin-commandline-reference.md | 68 ++++++ docs/reference/versions.md | 2 +- docs/usage/kubectl-plugin.md | 70 +++++++ examples/nodefeature.yaml | 119 +++++++++++ examples/nodefeaturerule.yaml | 6 +- go.mod | 2 +- pkg/apis/nfd/validate/validate.go | 89 ++++++++ pkg/kubectl-nfd/dryrun.go | 195 ++++++++++++++++++ pkg/kubectl-nfd/test.go | 79 +++++++ pkg/kubectl-nfd/validate.go | 94 +++++++++ 16 files changed, 981 insertions(+), 11 deletions(-) create mode 100644 cmd/kubectl-nfd/main.go create mode 100644 cmd/kubectl-nfd/subcmd/dryrun.go create mode 100644 cmd/kubectl-nfd/subcmd/root.go create mode 100644 cmd/kubectl-nfd/subcmd/test.go create mode 100644 cmd/kubectl-nfd/subcmd/validate.go create mode 100644 docs/reference/plugin-commandline-reference.md create mode 100644 docs/usage/kubectl-plugin.md create mode 100644 examples/nodefeature.yaml create mode 100644 pkg/kubectl-nfd/dryrun.go create mode 100644 pkg/kubectl-nfd/test.go create mode 100644 pkg/kubectl-nfd/validate.go diff --git a/Makefile b/Makefile index ca025da87..fb66d1599 100644 --- a/Makefile +++ b/Makefile @@ -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) diff --git a/cmd/kubectl-nfd/main.go b/cmd/kubectl-nfd/main.go new file mode 100644 index 000000000..5ba346485 --- /dev/null +++ b/cmd/kubectl-nfd/main.go @@ -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() +} diff --git a/cmd/kubectl-nfd/subcmd/dryrun.go b/cmd/kubectl-nfd/subcmd/dryrun.go new file mode 100644 index 000000000..6339dda31 --- /dev/null +++ b/cmd/kubectl-nfd/subcmd/dryrun.go @@ -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) + } +} diff --git a/cmd/kubectl-nfd/subcmd/root.go b/cmd/kubectl-nfd/subcmd/root.go new file mode 100644 index 000000000..0ab44149d --- /dev/null +++ b/cmd/kubectl-nfd/subcmd/root.go @@ -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) + } +} diff --git a/cmd/kubectl-nfd/subcmd/test.go b/cmd/kubectl-nfd/subcmd/test.go new file mode 100644 index 000000000..ba1a2f1b1 --- /dev/null +++ b/cmd/kubectl-nfd/subcmd/test.go @@ -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) + } +} diff --git a/cmd/kubectl-nfd/subcmd/validate.go b/cmd/kubectl-nfd/subcmd/validate.go new file mode 100644 index 000000000..9a2f84f15 --- /dev/null +++ b/cmd/kubectl-nfd/subcmd/validate.go @@ -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) + } +} diff --git a/docs/reference/plugin-commandline-reference.md b/docs/reference/plugin-commandline-reference.md new file mode 100644 index 000000000..8c7752e00 --- /dev/null +++ b/docs/reference/plugin-commandline-reference.md @@ -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. diff --git a/docs/reference/versions.md b/docs/reference/versions.md index 86b8bd4c1..9e9d36bca 100644 --- a/docs/reference/versions.md +++ b/docs/reference/versions.md @@ -1,7 +1,7 @@ --- title: "Versions" layout: default -sort: 8 +sort: 9 --- # Versions and deprecation diff --git a/docs/usage/kubectl-plugin.md b/docs/usage/kubectl-plugin.md new file mode 100644 index 000000000..3acf11781 --- /dev/null +++ b/docs/usage/kubectl-plugin.md @@ -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 +``` + +### Test + +The plugin can be used to test a NodeFeatureRule object against a node: + +```bash +kubectl nfd test -f -n +``` + +### DryRun + +The plugin can be used to DryRun a NodeFeatureRule object against a NodeFeature +file: + +```bash +kubectl get -n node-feature-discovery nodefeature -o yaml > +kubectl nfd dryrun -f -n +``` + +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" +``` diff --git a/examples/nodefeature.yaml b/examples/nodefeature.yaml new file mode 100644 index 000000000..3048d48f0 --- /dev/null +++ b/examples/nodefeature.yaml @@ -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" diff --git a/examples/nodefeaturerule.yaml b/examples/nodefeaturerule.yaml index 06fd662e6..4a646df59 100644 --- a/examples/nodefeaturerule.yaml +++ b/examples/nodefeaturerule.yaml @@ -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 } diff --git a/go.mod b/go.mod index 5d403d6c3..68fd8b0a8 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/pkg/apis/nfd/validate/validate.go b/pkg/apis/nfd/validate/validate.go index 5f68964fc..d903337ac 100644 --- a/pkg/apis/nfd/validate/validate.go +++ b/pkg/apis/nfd/validate/validate.go @@ -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 .), 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 { diff --git a/pkg/kubectl-nfd/dryrun.go b/pkg/kubectl-nfd/dryrun.go new file mode 100644 index 000000000..c502887ca --- /dev/null +++ b/pkg/kubectl-nfd/dryrun.go @@ -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 +} diff --git a/pkg/kubectl-nfd/test.go b/pkg/kubectl-nfd/test.go new file mode 100644 index 000000000..bafc83ab7 --- /dev/null +++ b/pkg/kubectl-nfd/test.go @@ -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 +} diff --git a/pkg/kubectl-nfd/validate.go b/pkg/kubectl-nfd/validate.go new file mode 100644 index 000000000..35424bd3f --- /dev/null +++ b/pkg/kubectl-nfd/validate.go @@ -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 +}