mirror of
https://github.com/kubernetes-sigs/node-feature-discovery.git
synced 2024-12-14 11:57:51 +00:00
Introduce nfd client tool with subset of image compatibility commands.
Signed-off-by: Marcin Franczyk <marcin0franczyk@gmail.com>
This commit is contained in:
parent
b4996ad399
commit
b9749cffe1
15 changed files with 1320 additions and 1 deletions
2
Makefile
2
Makefile
|
@ -90,7 +90,7 @@ IMAGE_BUILD_ARGS_MINIMAL = --target minimal \
|
|||
|
||||
all: image
|
||||
|
||||
BUILD_BINARIES := nfd-master nfd-worker nfd-topology-updater nfd-gc kubectl-nfd
|
||||
BUILD_BINARIES := nfd-master nfd-worker nfd-topology-updater nfd-gc kubectl-nfd nfd
|
||||
|
||||
build-%:
|
||||
$(GO_CMD) build -v -o bin/ $(BUILD_FLAGS) ./cmd/$*
|
||||
|
|
48
api/image-compatibility/v1alpha1/spec.go
Normal file
48
api/image-compatibility/v1alpha1/spec.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
Copyright 2024 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 v1alpha1
|
||||
|
||||
import (
|
||||
nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/api/nfd/v1alpha1"
|
||||
)
|
||||
|
||||
// ArtifactType is a type of OCI artifact that contains image compatibility metadata.
|
||||
const (
|
||||
ArtifactType = "application/vnd.nfd.image-compatibility.v1alpha1"
|
||||
Version = "v1alpha1"
|
||||
)
|
||||
|
||||
// Spec represents image compatibility metadata.
|
||||
type Spec struct {
|
||||
// Version of the spec.
|
||||
Version string `json:"version"`
|
||||
// Compatibilities contains list of compatibility sets.
|
||||
Compatibilties []Compatibility `json:"compatibilities"`
|
||||
}
|
||||
|
||||
// Compatibility represents image compatibility metadata
|
||||
// that describe the image requirements for the host and OS.
|
||||
type Compatibility struct {
|
||||
// Rules represents a list of Node Feature Rules.
|
||||
Rules []nfdv1alpha1.Rule `json:"rules"`
|
||||
// Weight indicates the priority of the compatibility set.
|
||||
Weight int `json:"weight,omitempty"`
|
||||
// Tag enables grouping or distinguishing between compatibility sets.
|
||||
Tag string `json:"tag,omitempty"`
|
||||
// Description of the compatibility set.
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
27
cmd/nfd/main.go
Normal file
27
cmd/nfd/main.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
Copyright 2024 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/nfd/subcmd"
|
||||
)
|
||||
|
||||
const ProgramName = "nfd"
|
||||
|
||||
func main() {
|
||||
subcmd.Execute()
|
||||
}
|
36
cmd/nfd/subcmd/compat/compat.go
Normal file
36
cmd/nfd/subcmd/compat/compat.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
Copyright 2024 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 compat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var CompatCmd = &cobra.Command{
|
||||
Use: "compat",
|
||||
Short: "Image compatibility commands",
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
if err := CompatCmd.Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
70
cmd/nfd/subcmd/compat/options/platform.go
Normal file
70
cmd/nfd/subcmd/compat/options/platform.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
Copyright 2024 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 options
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// PlatformOption represents
|
||||
type PlatformOption struct {
|
||||
// PlatformStr contains the raw platform argument provided by the user.
|
||||
PlatformStr string
|
||||
// Platform represents the OCI platform specification, built from PlatformStr.
|
||||
Platform *ocispec.Platform
|
||||
}
|
||||
|
||||
// Parse takes the PlatformStr argument provided by the user
|
||||
// to build OCI platform specification.
|
||||
func (opt *PlatformOption) Parse(*cobra.Command) error {
|
||||
var pStr string
|
||||
|
||||
if opt.PlatformStr == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
platform := &ocispec.Platform{}
|
||||
pStr, platform.OSVersion, _ = strings.Cut(opt.PlatformStr, ":")
|
||||
parts := strings.Split(pStr, "/")
|
||||
|
||||
switch len(parts) {
|
||||
case 3:
|
||||
platform.Variant = parts[2]
|
||||
fallthrough
|
||||
case 2:
|
||||
platform.Architecture = parts[1]
|
||||
case 1:
|
||||
platform.Architecture = runtime.GOARCH
|
||||
default:
|
||||
return fmt.Errorf("failed to parse platform %q: expected format os[/arch[/variant]]", opt.PlatformStr)
|
||||
}
|
||||
|
||||
platform.OS = parts[0]
|
||||
if platform.OS == "" {
|
||||
return fmt.Errorf("invalid platform: OS cannot be empty")
|
||||
}
|
||||
if platform.Architecture == "" {
|
||||
return fmt.Errorf("invalid platform: Architecture cannot be empty")
|
||||
}
|
||||
opt.Platform = platform
|
||||
return nil
|
||||
}
|
227
cmd/nfd/subcmd/compat/validate-node.go
Normal file
227
cmd/nfd/subcmd/compat/validate-node.go
Normal file
|
@ -0,0 +1,227 @@
|
|||
/*
|
||||
Copyright 2024 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 compat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/jedib0t/go-pretty/v6/text"
|
||||
"github.com/spf13/cobra"
|
||||
"oras.land/oras-go/v2/registry"
|
||||
|
||||
nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/api/nfd/v1alpha1"
|
||||
"sigs.k8s.io/node-feature-discovery/cmd/nfd/subcmd/compat/options"
|
||||
artifactcli "sigs.k8s.io/node-feature-discovery/pkg/client-nfd/compat/artifact-client"
|
||||
nodevalidator "sigs.k8s.io/node-feature-discovery/pkg/client-nfd/compat/node-validator"
|
||||
"sigs.k8s.io/node-feature-discovery/source"
|
||||
)
|
||||
|
||||
var (
|
||||
image string
|
||||
tags []string
|
||||
platform options.PlatformOption
|
||||
plainHTTP bool
|
||||
outputJSON bool
|
||||
|
||||
// secrets
|
||||
readPassword bool
|
||||
readAccessToken bool
|
||||
username string
|
||||
password string
|
||||
accessToken string
|
||||
)
|
||||
|
||||
var validateNodeCmd = &cobra.Command{
|
||||
Use: "validate-node",
|
||||
Short: "Perform node validation based on its associated image compatibility artifact",
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
|
||||
if err = platform.Parse(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if readAccessToken && readPassword {
|
||||
return fmt.Errorf("cannot use --read-access-token and --read-password at the same time")
|
||||
} else if readAccessToken {
|
||||
accessToken, err = readStdin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if readPassword {
|
||||
password, err = readStdin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
ref, err := registry.ParseReference(image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sources := map[string]source.FeatureSource{}
|
||||
for k, v := range source.GetAllFeatureSources() {
|
||||
if ts, ok := v.(source.SupplementalSource); ok && ts.DisableByDefault() {
|
||||
continue
|
||||
}
|
||||
sources[k] = v
|
||||
}
|
||||
|
||||
authOpt := artifactcli.WithAuthDefault()
|
||||
if username != "" && password != "" {
|
||||
authOpt = artifactcli.WithAuthPassword(username, password)
|
||||
} else if accessToken != "" {
|
||||
authOpt = artifactcli.WithAuthToken(accessToken)
|
||||
}
|
||||
|
||||
ac := artifactcli.New(
|
||||
&ref,
|
||||
artifactcli.WithArgs(artifactcli.Args{PlainHttp: plainHTTP}),
|
||||
artifactcli.WithPlatform(platform.Platform),
|
||||
authOpt,
|
||||
)
|
||||
|
||||
nv := nodevalidator.New(
|
||||
nodevalidator.WithArgs(&nodevalidator.Args{Tags: tags}),
|
||||
nodevalidator.WithArtifactClient(ac),
|
||||
nodevalidator.WithSources(sources),
|
||||
)
|
||||
|
||||
out, err := nv.Execute(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if outputJSON {
|
||||
b, err := json.Marshal(out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("%s", b)
|
||||
} else {
|
||||
pprintResult(out)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func readStdin() (string, error) {
|
||||
secretRaw, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
secret := strings.TrimSuffix(string(secretRaw), "\n")
|
||||
secret = strings.TrimSuffix(secret, "\r")
|
||||
|
||||
return secret, nil
|
||||
}
|
||||
|
||||
func pprintResult(css []*nodevalidator.CompatibilityStatus) {
|
||||
for i, cs := range css {
|
||||
fmt.Print(text.Colors{text.FgCyan, text.Bold}.Sprintf("COMPATIBILITY SET #%d ", i+1))
|
||||
fmt.Print(text.FgCyan.Sprintf("Weight: %d", cs.Weight))
|
||||
if cs.Tag != "" {
|
||||
fmt.Print(text.FgCyan.Sprintf("; Tag: %s", cs.Tag))
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Println(text.FgWhite.Sprintf("Description: %s", cs.Description))
|
||||
|
||||
for _, r := range cs.Rules {
|
||||
printTable(r)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
func printTable(rs nodevalidator.RuleStatus) {
|
||||
t := table.NewWriter()
|
||||
t.SetStyle(table.StyleLight)
|
||||
t.SetOutputMirror(os.Stdout)
|
||||
t.Style().Format.Header = text.FormatDefault
|
||||
t.SetAutoIndex(true)
|
||||
|
||||
validTxt := text.BgRed.Sprint(" FAIL ")
|
||||
if rs.IsMatch {
|
||||
validTxt = text.BgGreen.Sprint(" OK ")
|
||||
}
|
||||
ruleTxt := strings.ToUpper(fmt.Sprintf("rule: %s", rs.Rule.Name))
|
||||
|
||||
t.SetTitle(text.Bold.Sprintf("%s - %s", ruleTxt, validTxt))
|
||||
t.AppendHeader(table.Row{"Feature", "Expression", "Status"})
|
||||
|
||||
if mf := rs.Rule.MatchFeatures; len(mf) > 0 {
|
||||
renderMatchFeatures(t, mf)
|
||||
}
|
||||
if ma := rs.Rule.MatchAny; len(ma) > 0 {
|
||||
for _, mae := range ma {
|
||||
t.AppendSeparator()
|
||||
renderMatchFeatures(t, mae.MatchFeatures)
|
||||
}
|
||||
}
|
||||
t.Render()
|
||||
}
|
||||
|
||||
func renderMatchFeatures(t table.Writer, matchFeatures nfdv1alpha1.FeatureMatcher) {
|
||||
for _, fm := range matchFeatures {
|
||||
if fm.MatchExpressions != nil {
|
||||
for key, exp := range *fm.MatchExpressions {
|
||||
addTableRows(t, fmt.Sprintf("%s.%s", fm.Feature, key), exp.String(), exp.IsMatch)
|
||||
}
|
||||
}
|
||||
if exp := fm.MatchName; exp != nil {
|
||||
addTableRows(t, fm.Feature, exp.String(), exp.IsMatch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addTableRows(t table.Writer, fullFeatureDomain string, expression string, isMatch bool) {
|
||||
status := text.FgHiRed.Sprint("FAIL")
|
||||
if isMatch {
|
||||
status = text.FgHiGreen.Sprint("OK")
|
||||
}
|
||||
t.AppendRow(table.Row{fullFeatureDomain, expression, status})
|
||||
}
|
||||
|
||||
func init() {
|
||||
CompatCmd.AddCommand(validateNodeCmd)
|
||||
validateNodeCmd.Flags().StringVar(&image, "image", "", "the URL of the image containing compatibility metadata")
|
||||
validateNodeCmd.Flags().StringSliceVar(&tags, "tags", []string{}, "a list of tags that must match the tags set on the compatibility objects")
|
||||
validateNodeCmd.Flags().StringVar(&platform.PlatformStr, "platform", "", "the artifact platform in the format os[/arch][/variant][:os_version]")
|
||||
validateNodeCmd.Flags().BoolVar(&plainHTTP, "plain-http", false, "use of HTTP protocol for all registry communications")
|
||||
validateNodeCmd.Flags().BoolVar(&outputJSON, "output-json", false, "print a JSON object")
|
||||
validateNodeCmd.Flags().StringVar(&username, "reg-username", "", "registry username")
|
||||
validateNodeCmd.Flags().BoolVar(&readPassword, "reg-password-stdin", false, "read registry password from stdin")
|
||||
validateNodeCmd.Flags().BoolVar(&readAccessToken, "reg-token-stdin", false, "read registry access token from stdin")
|
||||
|
||||
if err := validateNodeCmd.MarkFlagRequired("image"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
45
cmd/nfd/subcmd/root.go
Normal file
45
cmd/nfd/subcmd/root.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
Copyright 2024 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"
|
||||
|
||||
"sigs.k8s.io/node-feature-discovery/cmd/nfd/subcmd/compat"
|
||||
)
|
||||
|
||||
// RootCmd represents the base command when called without any subcommands
|
||||
var RootCmd = &cobra.Command{
|
||||
Use: "nfd",
|
||||
Short: "Node Feature Discovery client",
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(compat.CompatCmd)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
27
examples/image-compatibility.yaml
Normal file
27
examples/image-compatibility.yaml
Normal file
|
@ -0,0 +1,27 @@
|
|||
version: v1alpha1
|
||||
compatibilities:
|
||||
- description: "my image requirements"
|
||||
rules:
|
||||
- name: "kernel and cpu"
|
||||
matchFeatures:
|
||||
- feature: kernel.loadedmodule
|
||||
matchExpressions:
|
||||
vfio-pci: {op: Exists}
|
||||
ip_tables: {op: Exists}
|
||||
- feature: cpu.model
|
||||
matchExpressions:
|
||||
vendor_id: {op: In, value: ["Intel", "AMD"]}
|
||||
- feature: cpu.cpuid
|
||||
matchName: {op: InRegexp, value: ["^AVX"]}
|
||||
- name: "one of available nics"
|
||||
matchAny:
|
||||
- matchFeatures:
|
||||
- feature: pci.device
|
||||
matchExpressions:
|
||||
vendor: {op: In, value: ["0eee"]}
|
||||
class: {op: In, value: ["0200"]}
|
||||
- matchFeatures:
|
||||
- feature: pci.device
|
||||
matchExpressions:
|
||||
vendor: {op: In, value: ["0fff"]}
|
||||
class: {op: In, value: ["0200"]}
|
5
go.mod
5
go.mod
|
@ -7,11 +7,13 @@ require (
|
|||
github.com/google/go-cmp v0.6.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/jaypipes/ghw v0.13.0
|
||||
github.com/jedib0t/go-pretty/v6 v6.6.1
|
||||
github.com/k8stopologyawareschedwg/noderesourcetopology-api v0.1.2
|
||||
github.com/k8stopologyawareschedwg/podfingerprint v0.2.2
|
||||
github.com/klauspost/cpuid/v2 v2.2.9
|
||||
github.com/onsi/ginkgo/v2 v2.22.0
|
||||
github.com/onsi/gomega v1.36.0
|
||||
github.com/opencontainers/image-spec v1.1.0
|
||||
github.com/opencontainers/runc v1.2.2
|
||||
github.com/prometheus/client_golang v1.19.1
|
||||
github.com/smartystreets/goconvey v1.8.1
|
||||
|
@ -34,6 +36,7 @@ require (
|
|||
k8s.io/kubernetes v1.31.3
|
||||
k8s.io/pod-security-admission v0.31.3
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8
|
||||
oras.land/oras-go/v2 v2.5.0
|
||||
sigs.k8s.io/node-feature-discovery/api/nfd v0.0.0-00010101000000-000000000000
|
||||
sigs.k8s.io/yaml v1.4.0
|
||||
)
|
||||
|
@ -83,6 +86,7 @@ require (
|
|||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/jtolds/gls v4.20.0+incompatible // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/moby/spdystream v0.4.0 // indirect
|
||||
github.com/moby/sys/mountinfo v0.7.1 // indirect
|
||||
|
@ -98,6 +102,7 @@ require (
|
|||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.55.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/smarty/assertions v1.15.1 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
|
|
10
go.sum
10
go.sum
|
@ -124,6 +124,8 @@ github.com/jaypipes/ghw v0.13.0 h1:log8MXuB8hzTNnSktqpXMHc0c/2k/WgjOMSUtnI1RV4=
|
|||
github.com/jaypipes/ghw v0.13.0/go.mod h1:In8SsaDqlb1oTyrbmTC14uy+fbBMvp+xdqX51MidlD8=
|
||||
github.com/jaypipes/pcidb v1.0.1 h1:WB2zh27T3nwg8AE8ei81sNRb9yWBii3JGNJtT7K9Oic=
|
||||
github.com/jaypipes/pcidb v1.0.1/go.mod h1:6xYUz/yYEyOkIkUt2t2J2folIuZ4Yg6uByCGFXMCeE4=
|
||||
github.com/jedib0t/go-pretty/v6 v6.6.1 h1:iJ65Xjb680rHcikRj6DSIbzCex2huitmc7bDtxYVWyc=
|
||||
github.com/jedib0t/go-pretty/v6 v6.6.1/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
|
||||
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
|
||||
|
@ -152,6 +154,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible h1:aKW/4cBs+yK6gpqU3K/oIwk9Q/XICqd3zOX/UFuvqmk=
|
||||
github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
|
@ -181,6 +185,8 @@ github.com/onsi/gomega v1.36.0 h1:Pb12RlruUtj4XUuPUqeEWc6j5DkVVVA49Uf6YLfC95Y=
|
|||
github.com/onsi/gomega v1.36.0/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/opencontainers/runc v1.2.2 h1:jTg3Vw2A5f0N9PoxFTEwUhvpANGaNPT3689Yfd/zaX0=
|
||||
github.com/opencontainers/runc v1.2.2/go.mod h1:/PXzF0h531HTMsYQnmxXkBD7YaGShm/2zcRB79dksUc=
|
||||
github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk=
|
||||
|
@ -200,6 +206,8 @@ github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G
|
|||
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
|
@ -417,6 +425,8 @@ k8s.io/pod-security-admission v0.31.3 h1:8NzEV0HtdStX367AuSKfRMIZHn0hT4xuz8xNEf7
|
|||
k8s.io/pod-security-admission v0.31.3/go.mod h1:YMIcTe/7f9R9d+3ErCMMM3Wtbj9ejKo7Z9S0OxZQrRg=
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
oras.land/oras-go/v2 v2.5.0 h1:o8Me9kLY74Vp5uw07QXPiitjsw7qNXi8Twd+19Zf02c=
|
||||
oras.land/oras-go/v2 v2.5.0/go.mod h1:z4eisnLP530vwIOUOJeBIj0aGI0L1C3d53atvCBqZHg=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 h1:2770sDpzrjjsAtVhSeUFseziht227YAWYHLGNM8QPwY=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
|
||||
|
|
178
pkg/client-nfd/compat/artifact-client/client.go
Normal file
178
pkg/client-nfd/compat/artifact-client/client.go
Normal file
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
Copyright 2024 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 compat
|
||||
|
||||
//go:generate mockery --name=ArtifactClient --inpackage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
oras "oras.land/oras-go/v2"
|
||||
"oras.land/oras-go/v2/registry"
|
||||
"oras.land/oras-go/v2/registry/remote"
|
||||
"oras.land/oras-go/v2/registry/remote/auth"
|
||||
"oras.land/oras-go/v2/registry/remote/retry"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
compatv1alpha1 "sigs.k8s.io/node-feature-discovery/api/image-compatibility/v1alpha1"
|
||||
)
|
||||
|
||||
// ArtifactClient interface contain set of functions to manipulate compatibility artfact.
|
||||
type ArtifactClient interface {
|
||||
// FetchCompatibilitySpec downloads the compatibility specifcation associated with the image.
|
||||
FetchCompatibilitySpec(ctx context.Context) (*compatv1alpha1.Spec, error)
|
||||
}
|
||||
|
||||
// Args holds command line arguments.
|
||||
type Args struct {
|
||||
PlainHttp bool
|
||||
}
|
||||
|
||||
// Client represents a client that is reposnible for all artifact operations.
|
||||
type Client struct {
|
||||
Args Args
|
||||
RegReference *registry.Reference
|
||||
Platform *ocispec.Platform
|
||||
|
||||
orasClient *auth.Client
|
||||
}
|
||||
|
||||
// New returns a new compatibility spec object.
|
||||
func New(regReference *registry.Reference, opts ...ArtifactClientOpts) *Client {
|
||||
c := &Client{
|
||||
RegReference: regReference,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt.apply(c)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// FetchCompatibilitySpec pulls the image compatibility specification associated with the image.
|
||||
func (c *Client) FetchCompatibilitySpec(ctx context.Context) (*compatv1alpha1.Spec, error) {
|
||||
repo, err := remote.NewRepository(c.RegReference.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repo.Client = c.orasClient
|
||||
repo.PlainHTTP = c.Args.PlainHttp
|
||||
|
||||
opts := oras.DefaultResolveOptions
|
||||
if c.Platform != nil {
|
||||
opts.TargetPlatform = c.Platform
|
||||
}
|
||||
|
||||
targetDesc, err := oras.Resolve(ctx, repo, c.RegReference.Reference, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
descs, err := registry.Referrers(ctx, repo, targetDesc, compatv1alpha1.ArtifactType)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
} else if len(descs) < 1 {
|
||||
return nil, fmt.Errorf("compatibility artifact not found")
|
||||
}
|
||||
artifactDesc := descs[len(descs)-1]
|
||||
|
||||
_, content, err := oras.FetchBytes(ctx, repo.Manifests(), artifactDesc.Digest.String(), oras.DefaultFetchBytesOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
manifest := ocispec.Manifest{}
|
||||
if err := json.Unmarshal(content, &manifest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: now it's a lazy check, verify in the future the media types and number of layers
|
||||
if len(manifest.Layers) < 1 {
|
||||
return nil, fmt.Errorf("compatibility layer not found")
|
||||
}
|
||||
specDesc := manifest.Layers[0]
|
||||
|
||||
_, compatSpecRaw, err := oras.FetchBytes(ctx, repo.Blobs(), specDesc.Digest.String(), oras.DefaultFetchBytesOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
compatSpec := compatv1alpha1.Spec{}
|
||||
err = yaml.Unmarshal(compatSpecRaw, &compatSpec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &compatSpec, nil
|
||||
}
|
||||
|
||||
// NodeValidatorOpts applies certain options to the node validator.
|
||||
type ArtifactClientOpts interface {
|
||||
apply(*Client)
|
||||
}
|
||||
|
||||
type artifactClientOpt struct {
|
||||
f func(*Client)
|
||||
}
|
||||
|
||||
func (o *artifactClientOpt) apply(nv *Client) {
|
||||
o.f(nv)
|
||||
}
|
||||
|
||||
// WithArgs applies arguments to the artifact client.
|
||||
func WithArgs(args Args) ArtifactClientOpts {
|
||||
return &artifactClientOpt{f: func(c *Client) { c.Args = args }}
|
||||
}
|
||||
|
||||
// WithPlatform applies OCI platform spec to the artifact client.
|
||||
func WithPlatform(platform *ocispec.Platform) ArtifactClientOpts {
|
||||
return &artifactClientOpt{f: func(c *Client) { c.Platform = platform }}
|
||||
}
|
||||
|
||||
// WithAuthPassword initializes oras client with user and password.
|
||||
func WithAuthPassword(username, password string) ArtifactClientOpts {
|
||||
return &artifactClientOpt{f: func(c *Client) {
|
||||
c.orasClient = &auth.Client{
|
||||
Client: retry.DefaultClient,
|
||||
Cache: auth.NewCache(),
|
||||
Credential: auth.StaticCredential(c.RegReference.Registry, auth.Credential{
|
||||
Username: username,
|
||||
Password: password,
|
||||
}),
|
||||
}
|
||||
}}
|
||||
}
|
||||
|
||||
// WithAuthToken initializes oras client with auth token.
|
||||
func WithAuthToken(token string) ArtifactClientOpts {
|
||||
return &artifactClientOpt{f: func(c *Client) {
|
||||
c.orasClient = &auth.Client{
|
||||
Client: retry.DefaultClient,
|
||||
Cache: auth.NewCache(),
|
||||
Credential: auth.StaticCredential(c.RegReference.Registry, auth.Credential{
|
||||
AccessToken: token,
|
||||
}),
|
||||
}
|
||||
}}
|
||||
}
|
||||
|
||||
// WithAuthDefault initializes the default oras client that does not authenticate.
|
||||
func WithAuthDefault() ArtifactClientOpts {
|
||||
return &artifactClientOpt{f: func(c *Client) { c.orasClient = auth.DefaultClient }}
|
||||
}
|
59
pkg/client-nfd/compat/artifact-client/mock_ArtifactClient.go
Normal file
59
pkg/client-nfd/compat/artifact-client/mock_ArtifactClient.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
// Code generated by mockery v2.42.0. DO NOT EDIT.
|
||||
|
||||
package compat
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
v1alpha1 "sigs.k8s.io/node-feature-discovery/api/image-compatibility/v1alpha1"
|
||||
)
|
||||
|
||||
// MockArtifactClient is an autogenerated mock type for the ArtifactClient type
|
||||
type MockArtifactClient struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// FetchCompatibilitySpec provides a mock function with given fields: ctx
|
||||
func (_m *MockArtifactClient) FetchCompatibilitySpec(ctx context.Context) (*v1alpha1.Spec, error) {
|
||||
ret := _m.Called(ctx)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for FetchCompatibilitySpec")
|
||||
}
|
||||
|
||||
var r0 *v1alpha1.Spec
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context) (*v1alpha1.Spec, error)); ok {
|
||||
return rf(ctx)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context) *v1alpha1.Spec); ok {
|
||||
r0 = rf(ctx)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*v1alpha1.Spec)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
|
||||
r1 = rf(ctx)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// NewMockArtifactClient creates a new instance of MockArtifactClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockArtifactClient(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockArtifactClient {
|
||||
mock := &MockArtifactClient{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
127
pkg/client-nfd/compat/node-validator/node-validator.go
Normal file
127
pkg/client-nfd/compat/node-validator/node-validator.go
Normal file
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
Copyright 2024 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 nodevalidator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/api/nfd/v1alpha1"
|
||||
"sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/nodefeaturerule"
|
||||
artifactcli "sigs.k8s.io/node-feature-discovery/pkg/client-nfd/compat/artifact-client"
|
||||
"sigs.k8s.io/node-feature-discovery/source"
|
||||
|
||||
// register sources
|
||||
_ "sigs.k8s.io/node-feature-discovery/source/cpu"
|
||||
_ "sigs.k8s.io/node-feature-discovery/source/kernel"
|
||||
_ "sigs.k8s.io/node-feature-discovery/source/memory"
|
||||
_ "sigs.k8s.io/node-feature-discovery/source/network"
|
||||
_ "sigs.k8s.io/node-feature-discovery/source/pci"
|
||||
_ "sigs.k8s.io/node-feature-discovery/source/storage"
|
||||
_ "sigs.k8s.io/node-feature-discovery/source/system"
|
||||
_ "sigs.k8s.io/node-feature-discovery/source/usb"
|
||||
)
|
||||
|
||||
// Args holds command line arguments.
|
||||
type Args struct {
|
||||
Tags []string
|
||||
}
|
||||
|
||||
type nodeValidator struct {
|
||||
args Args
|
||||
|
||||
artifactClient artifactcli.ArtifactClient
|
||||
sources map[string]source.FeatureSource
|
||||
}
|
||||
|
||||
// New builds a node validator with specified options.
|
||||
func New(opts ...NodeValidatorOpts) nodeValidator {
|
||||
n := nodeValidator{}
|
||||
for _, opt := range opts {
|
||||
opt.apply(&n)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// Execute pulls the compatibility artifact to compare described features with the ones discovered on the node.
|
||||
func (nv *nodeValidator) Execute(ctx context.Context) ([]*CompatibilityStatus, error) {
|
||||
spec, err := nv.artifactClient.FetchCompatibilitySpec(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, s := range nv.sources {
|
||||
if err := s.Discover(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
features := source.GetAllFeatures()
|
||||
|
||||
compats := []*CompatibilityStatus{}
|
||||
for _, c := range spec.Compatibilties {
|
||||
if len(nv.args.Tags) > 0 && !slices.Contains(nv.args.Tags, c.Tag) {
|
||||
continue
|
||||
}
|
||||
compat := newCompatibilityStatus(&c)
|
||||
|
||||
for _, r := range c.Rules {
|
||||
ruleOut, err := nodefeaturerule.Execute(&r, features, nodefeaturerule.RunAllStrategy{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
compat.Rules = append(compat.Rules, RuleStatus{
|
||||
Rule: &r,
|
||||
IsMatch: ruleOut.IsMatch,
|
||||
})
|
||||
|
||||
// Add the 'rule.matched' feature for backreference functionality
|
||||
features.InsertAttributeFeatures(nfdv1alpha1.RuleBackrefDomain, nfdv1alpha1.RuleBackrefFeature, ruleOut.Labels)
|
||||
features.InsertAttributeFeatures(nfdv1alpha1.RuleBackrefDomain, nfdv1alpha1.RuleBackrefFeature, ruleOut.Vars)
|
||||
}
|
||||
compats = append(compats, &compat)
|
||||
}
|
||||
|
||||
return compats, nil
|
||||
}
|
||||
|
||||
// NodeValidatorOpts applies certain options to the node validator.
|
||||
type NodeValidatorOpts interface {
|
||||
apply(*nodeValidator)
|
||||
}
|
||||
|
||||
type nodeValidatorOpt struct {
|
||||
f func(*nodeValidator)
|
||||
}
|
||||
|
||||
func (o *nodeValidatorOpt) apply(nv *nodeValidator) {
|
||||
o.f(nv)
|
||||
}
|
||||
|
||||
// WithArgs applies command line arguments to the node validator object.
|
||||
func WithArgs(args *Args) NodeValidatorOpts {
|
||||
return &nodeValidatorOpt{f: func(nv *nodeValidator) { nv.args = *args }}
|
||||
}
|
||||
|
||||
// WithArtifactClient applies the client for all artifact operations.
|
||||
func WithArtifactClient(cli artifactcli.ArtifactClient) NodeValidatorOpts {
|
||||
return &nodeValidatorOpt{f: func(nv *nodeValidator) { nv.artifactClient = cli }}
|
||||
}
|
||||
|
||||
// WithSources applies the list of enabled feature sources.
|
||||
func WithSources(sources map[string]source.FeatureSource) NodeValidatorOpts {
|
||||
return &nodeValidatorOpt{f: func(nv *nodeValidator) { nv.sources = sources }}
|
||||
}
|
407
pkg/client-nfd/compat/node-validator/node-validator_test.go
Normal file
407
pkg/client-nfd/compat/node-validator/node-validator_test.go
Normal file
|
@ -0,0 +1,407 @@
|
|||
/*
|
||||
Copyright 2024 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 nodevalidator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
compatv1alpha1 "sigs.k8s.io/node-feature-discovery/api/image-compatibility/v1alpha1"
|
||||
"sigs.k8s.io/node-feature-discovery/api/nfd/v1alpha1"
|
||||
artifactcli "sigs.k8s.io/node-feature-discovery/pkg/client-nfd/compat/artifact-client"
|
||||
"sigs.k8s.io/node-feature-discovery/source"
|
||||
"sigs.k8s.io/node-feature-discovery/source/fake"
|
||||
)
|
||||
|
||||
func init() {
|
||||
fs := source.GetConfigurableSource(fake.Name)
|
||||
fs.SetConfig(fs.NewConfig())
|
||||
}
|
||||
|
||||
func TestNodeValidator(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
Convey("With a single compatibility set that contains flags, attributes and instances", t, func() {
|
||||
spec := &compatv1alpha1.Spec{
|
||||
Version: compatv1alpha1.Version,
|
||||
Compatibilties: []compatv1alpha1.Compatibility{
|
||||
{
|
||||
Description: "Fake compatibility",
|
||||
Rules: []v1alpha1.Rule{
|
||||
{
|
||||
Name: "fake_1",
|
||||
MatchFeatures: v1alpha1.FeatureMatcher{
|
||||
{
|
||||
Feature: "fake.flag",
|
||||
MatchName: &v1alpha1.MatchExpression{Op: v1alpha1.MatchInRegexp, Value: v1alpha1.MatchValue{"^flag"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "fake_2",
|
||||
MatchFeatures: v1alpha1.FeatureMatcher{
|
||||
{
|
||||
Feature: "fake.flag",
|
||||
MatchExpressions: &v1alpha1.MatchExpressionSet{
|
||||
"flag_unkown": &v1alpha1.MatchExpression{Op: v1alpha1.MatchExists},
|
||||
},
|
||||
},
|
||||
{
|
||||
Feature: "fake.attribute",
|
||||
MatchExpressions: &v1alpha1.MatchExpressionSet{
|
||||
"attr_1": &v1alpha1.MatchExpression{Op: v1alpha1.MatchIn, Value: v1alpha1.MatchValue{"true"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "fake_3",
|
||||
MatchFeatures: v1alpha1.FeatureMatcher{
|
||||
{
|
||||
Feature: "fake.instance",
|
||||
MatchExpressions: &v1alpha1.MatchExpressionSet{
|
||||
"name": &v1alpha1.MatchExpression{Op: v1alpha1.MatchIn, Value: v1alpha1.MatchValue{"instance_1"}},
|
||||
"attr_1": &v1alpha1.MatchExpression{Op: v1alpha1.MatchIn, Value: v1alpha1.MatchValue{"true"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
Feature: "fake.instance",
|
||||
MatchExpressions: &v1alpha1.MatchExpressionSet{
|
||||
"name": &v1alpha1.MatchExpression{Op: v1alpha1.MatchIn, Value: v1alpha1.MatchValue{"instance_2"}},
|
||||
"attr_1": &v1alpha1.MatchExpression{Op: v1alpha1.MatchIn, Value: v1alpha1.MatchValue{"false"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "fake_4",
|
||||
MatchAny: []v1alpha1.MatchAnyElem{
|
||||
{
|
||||
MatchFeatures: v1alpha1.FeatureMatcher{
|
||||
{
|
||||
Feature: "fake.instance",
|
||||
MatchExpressions: &v1alpha1.MatchExpressionSet{
|
||||
"name": &v1alpha1.MatchExpression{Op: v1alpha1.MatchIn, Value: v1alpha1.MatchValue{"instance_1"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
MatchFeatures: v1alpha1.FeatureMatcher{
|
||||
{
|
||||
Feature: "fake.instance",
|
||||
MatchExpressions: &v1alpha1.MatchExpressionSet{
|
||||
"name": &v1alpha1.MatchExpression{Op: v1alpha1.MatchIn, Value: v1alpha1.MatchValue{"instance_unknown"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expectedOutput := []*CompatibilityStatus{
|
||||
{
|
||||
Description: "Fake compatibility",
|
||||
Rules: []RuleStatus{
|
||||
{
|
||||
Rule: &v1alpha1.Rule{
|
||||
Name: "fake_1",
|
||||
MatchFeatures: v1alpha1.FeatureMatcher{
|
||||
{
|
||||
Feature: "fake.flag",
|
||||
MatchName: &v1alpha1.MatchExpression{Op: v1alpha1.MatchInRegexp, Value: v1alpha1.MatchValue{"^flag"}, IsMatch: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
IsMatch: true,
|
||||
},
|
||||
{
|
||||
Rule: &v1alpha1.Rule{
|
||||
Name: "fake_2",
|
||||
MatchFeatures: v1alpha1.FeatureMatcher{
|
||||
{
|
||||
Feature: "fake.flag",
|
||||
MatchExpressions: &v1alpha1.MatchExpressionSet{
|
||||
"flag_unkown": &v1alpha1.MatchExpression{Op: v1alpha1.MatchExists, IsMatch: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
Feature: "fake.attribute",
|
||||
MatchExpressions: &v1alpha1.MatchExpressionSet{
|
||||
"attr_1": &v1alpha1.MatchExpression{Op: v1alpha1.MatchIn, Value: v1alpha1.MatchValue{"true"}, IsMatch: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
IsMatch: false,
|
||||
},
|
||||
{
|
||||
Rule: &v1alpha1.Rule{
|
||||
Name: "fake_3",
|
||||
MatchFeatures: v1alpha1.FeatureMatcher{
|
||||
{
|
||||
Feature: "fake.instance",
|
||||
MatchExpressions: &v1alpha1.MatchExpressionSet{
|
||||
"name": &v1alpha1.MatchExpression{Op: v1alpha1.MatchIn, Value: v1alpha1.MatchValue{"instance_1"}, IsMatch: true},
|
||||
"attr_1": &v1alpha1.MatchExpression{Op: v1alpha1.MatchIn, Value: v1alpha1.MatchValue{"true"}, IsMatch: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
Feature: "fake.instance",
|
||||
MatchExpressions: &v1alpha1.MatchExpressionSet{
|
||||
"name": &v1alpha1.MatchExpression{Op: v1alpha1.MatchIn, Value: v1alpha1.MatchValue{"instance_2"}, IsMatch: true},
|
||||
"attr_1": &v1alpha1.MatchExpression{Op: v1alpha1.MatchIn, Value: v1alpha1.MatchValue{"false"}, IsMatch: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
IsMatch: false,
|
||||
},
|
||||
{
|
||||
Rule: &v1alpha1.Rule{
|
||||
Name: "fake_4",
|
||||
MatchAny: []v1alpha1.MatchAnyElem{
|
||||
{
|
||||
MatchFeatures: v1alpha1.FeatureMatcher{
|
||||
{
|
||||
Feature: "fake.instance",
|
||||
MatchExpressions: &v1alpha1.MatchExpressionSet{
|
||||
"name": &v1alpha1.MatchExpression{Op: v1alpha1.MatchIn, Value: v1alpha1.MatchValue{"instance_1"}, IsMatch: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
MatchFeatures: v1alpha1.FeatureMatcher{
|
||||
{
|
||||
Feature: "fake.instance",
|
||||
MatchExpressions: &v1alpha1.MatchExpressionSet{
|
||||
"name": &v1alpha1.MatchExpression{Op: v1alpha1.MatchIn, Value: v1alpha1.MatchValue{"instance_unknown"}, IsMatch: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
IsMatch: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
validator := New(
|
||||
WithArgs(&Args{}),
|
||||
WithArtifactClient(newMock(ctx, spec)),
|
||||
WithSources(map[string]source.FeatureSource{fake.Name: source.GetFeatureSource(fake.Name)}),
|
||||
)
|
||||
output, err := validator.Execute(ctx)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(output, ShouldEqual, expectedOutput)
|
||||
})
|
||||
|
||||
Convey("With multiple compatibility sets", t, func() {
|
||||
spec := &compatv1alpha1.Spec{
|
||||
Version: compatv1alpha1.Version,
|
||||
Compatibilties: []compatv1alpha1.Compatibility{
|
||||
{
|
||||
Tag: "prefered",
|
||||
Weight: 90,
|
||||
Description: "Fake compatibility 1",
|
||||
Rules: []v1alpha1.Rule{
|
||||
{
|
||||
Name: "fake_1",
|
||||
MatchFeatures: v1alpha1.FeatureMatcher{
|
||||
{
|
||||
Feature: "fake.attribute",
|
||||
MatchExpressions: &v1alpha1.MatchExpressionSet{
|
||||
"attr_1": &v1alpha1.MatchExpression{Op: v1alpha1.MatchIn, Value: v1alpha1.MatchValue{"false"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Tag: "fallback",
|
||||
Weight: 40,
|
||||
Description: "Fake compatibility 2",
|
||||
Rules: []v1alpha1.Rule{
|
||||
{
|
||||
Name: "fake_1",
|
||||
MatchFeatures: v1alpha1.FeatureMatcher{
|
||||
{
|
||||
Feature: "fake.attribute",
|
||||
MatchExpressions: &v1alpha1.MatchExpressionSet{
|
||||
"attr_2": &v1alpha1.MatchExpression{Op: v1alpha1.MatchIn, Value: v1alpha1.MatchValue{"false"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expectedOutput := []*CompatibilityStatus{
|
||||
{
|
||||
Tag: "prefered",
|
||||
Weight: 90,
|
||||
Description: "Fake compatibility 1",
|
||||
Rules: []RuleStatus{
|
||||
{
|
||||
Rule: &v1alpha1.Rule{
|
||||
Name: "fake_1",
|
||||
MatchFeatures: v1alpha1.FeatureMatcher{
|
||||
{
|
||||
Feature: "fake.attribute",
|
||||
MatchExpressions: &v1alpha1.MatchExpressionSet{
|
||||
"attr_1": &v1alpha1.MatchExpression{Op: v1alpha1.MatchIn, Value: v1alpha1.MatchValue{"false"}, IsMatch: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
IsMatch: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Tag: "fallback",
|
||||
Weight: 40,
|
||||
Description: "Fake compatibility 2",
|
||||
Rules: []RuleStatus{
|
||||
{
|
||||
Rule: &v1alpha1.Rule{
|
||||
Name: "fake_1",
|
||||
MatchFeatures: v1alpha1.FeatureMatcher{
|
||||
{
|
||||
Feature: "fake.attribute",
|
||||
MatchExpressions: &v1alpha1.MatchExpressionSet{
|
||||
"attr_2": &v1alpha1.MatchExpression{Op: v1alpha1.MatchIn, Value: v1alpha1.MatchValue{"false"}, IsMatch: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
IsMatch: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
validator := New(
|
||||
WithArgs(&Args{}),
|
||||
WithArtifactClient(newMock(ctx, spec)),
|
||||
WithSources(map[string]source.FeatureSource{fake.Name: source.GetFeatureSource(fake.Name)}),
|
||||
)
|
||||
output, err := validator.Execute(ctx)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(output, ShouldEqual, expectedOutput)
|
||||
})
|
||||
|
||||
Convey("With compatibility sets filtered out by tags", t, func() {
|
||||
spec := &compatv1alpha1.Spec{
|
||||
Version: compatv1alpha1.Version,
|
||||
Compatibilties: []compatv1alpha1.Compatibility{
|
||||
{
|
||||
Tag: "prefered",
|
||||
Weight: 90,
|
||||
Description: "Fake compatibility 1",
|
||||
Rules: []v1alpha1.Rule{
|
||||
{
|
||||
Name: "fake_1",
|
||||
MatchFeatures: v1alpha1.FeatureMatcher{
|
||||
{
|
||||
Feature: "fake.attribute",
|
||||
MatchExpressions: &v1alpha1.MatchExpressionSet{
|
||||
"attr_1": &v1alpha1.MatchExpression{Op: v1alpha1.MatchIn, Value: v1alpha1.MatchValue{"false"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Tag: "fallback",
|
||||
Weight: 40,
|
||||
Description: "Fake compatibility 2",
|
||||
Rules: []v1alpha1.Rule{
|
||||
{
|
||||
Name: "fake_1",
|
||||
MatchFeatures: v1alpha1.FeatureMatcher{
|
||||
{
|
||||
Feature: "fake.attribute",
|
||||
MatchExpressions: &v1alpha1.MatchExpressionSet{
|
||||
"attr_2": &v1alpha1.MatchExpression{Op: v1alpha1.MatchIn, Value: v1alpha1.MatchValue{"false"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expectedOutput := []*CompatibilityStatus{
|
||||
{
|
||||
Tag: "prefered",
|
||||
Weight: 90,
|
||||
Description: "Fake compatibility 1",
|
||||
Rules: []RuleStatus{
|
||||
{
|
||||
Rule: &v1alpha1.Rule{
|
||||
Name: "fake_1",
|
||||
MatchFeatures: v1alpha1.FeatureMatcher{
|
||||
{
|
||||
Feature: "fake.attribute",
|
||||
MatchExpressions: &v1alpha1.MatchExpressionSet{
|
||||
"attr_1": &v1alpha1.MatchExpression{Op: v1alpha1.MatchIn, Value: v1alpha1.MatchValue{"false"}, IsMatch: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
IsMatch: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
validator := New(
|
||||
WithArgs(&Args{
|
||||
Tags: []string{"prefered"},
|
||||
}),
|
||||
WithArtifactClient(newMock(ctx, spec)),
|
||||
WithSources(map[string]source.FeatureSource{fake.Name: source.GetFeatureSource(fake.Name)}),
|
||||
)
|
||||
output, err := validator.Execute(ctx)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(output, ShouldEqual, expectedOutput)
|
||||
})
|
||||
}
|
||||
|
||||
func newMock(ctx context.Context, result *compatv1alpha1.Spec) *artifactcli.MockArtifactClient {
|
||||
artifactClient := &artifactcli.MockArtifactClient{}
|
||||
artifactClient.On("FetchCompatibilitySpec", ctx).Return(result, nil)
|
||||
return artifactClient
|
||||
}
|
53
pkg/client-nfd/compat/node-validator/status.go
Normal file
53
pkg/client-nfd/compat/node-validator/status.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
Copyright 2024 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 nodevalidator
|
||||
|
||||
import (
|
||||
compatv1alpha1 "sigs.k8s.io/node-feature-discovery/api/image-compatibility/v1alpha1"
|
||||
nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/api/nfd/v1alpha1"
|
||||
)
|
||||
|
||||
// CompatibilityStatus represents the state of
|
||||
// feature matching between the image and the host.
|
||||
type CompatibilityStatus struct {
|
||||
// Rules contain information about the matching status
|
||||
// of all Node Feature Rules.
|
||||
Rules []RuleStatus `json:"rules"`
|
||||
// Description of the compatibility set.
|
||||
Description string `json:"description,omitempty"`
|
||||
// Weight provides information about the priority of the compatibility set.
|
||||
Weight int `json:"weight,omitempty"`
|
||||
// Tag provides information about the tag assigned to the compatibility set.
|
||||
Tag string `json:"tag,omitempty"`
|
||||
}
|
||||
|
||||
func newCompatibilityStatus(c *compatv1alpha1.Compatibility) CompatibilityStatus {
|
||||
cs := CompatibilityStatus{
|
||||
Description: c.Description,
|
||||
Weight: c.Weight,
|
||||
Tag: c.Tag,
|
||||
}
|
||||
|
||||
return cs
|
||||
}
|
||||
|
||||
// RuleStatus contains information about features matching.
|
||||
type RuleStatus struct {
|
||||
*nfdv1alpha1.Rule
|
||||
// IsMatch provides information if the rule matches with the host.
|
||||
IsMatch bool `json:"isMatch"`
|
||||
}
|
Loading…
Reference in a new issue