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:
commit
0ca16c01e8
16 changed files with 981 additions and 11 deletions
17
Makefile
17
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)
|
||||
|
|
28
cmd/kubectl-nfd/main.go
Normal file
28
cmd/kubectl-nfd/main.go
Normal 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()
|
||||
}
|
59
cmd/kubectl-nfd/subcmd/dryrun.go
Normal file
59
cmd/kubectl-nfd/subcmd/dryrun.go
Normal 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)
|
||||
}
|
||||
}
|
54
cmd/kubectl-nfd/subcmd/root.go
Normal file
54
cmd/kubectl-nfd/subcmd/root.go
Normal 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)
|
||||
}
|
||||
}
|
56
cmd/kubectl-nfd/subcmd/test.go
Normal file
56
cmd/kubectl-nfd/subcmd/test.go
Normal 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)
|
||||
}
|
||||
}
|
54
cmd/kubectl-nfd/subcmd/validate.go
Normal file
54
cmd/kubectl-nfd/subcmd/validate.go
Normal 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)
|
||||
}
|
||||
}
|
68
docs/reference/plugin-commandline-reference.md
Normal file
68
docs/reference/plugin-commandline-reference.md
Normal 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.
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: "Versions"
|
||||
layout: default
|
||||
sort: 8
|
||||
sort: 9
|
||||
---
|
||||
|
||||
# Versions and deprecation
|
||||
|
|
70
docs/usage/kubectl-plugin.md
Normal file
70
docs/usage/kubectl-plugin.md
Normal 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
119
examples/nodefeature.yaml
Normal 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"
|
|
@ -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
2
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
|
||||
|
|
|
@ -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
195
pkg/kubectl-nfd/dryrun.go
Normal 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
79
pkg/kubectl-nfd/test.go
Normal 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
|
||||
}
|
94
pkg/kubectl-nfd/validate.go
Normal file
94
pkg/kubectl-nfd/validate.go
Normal 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
|
||||
}
|
Loading…
Reference in a new issue