Skip to content

Commit efc299e

Browse files
committed
Introduce nfd client tool with a subset of image compatibility commands
Signed-off-by: Marcin Franczyk <[email protected]>
1 parent 51bbbe2 commit efc299e

File tree

14 files changed

+1390
-1
lines changed

14 files changed

+1390
-1
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ IMAGE_BUILD_ARGS_MINIMAL = --target minimal \
9090

9191
all: image
9292

93-
BUILD_BINARIES := nfd-master nfd-worker nfd-topology-updater nfd-gc kubectl-nfd
93+
BUILD_BINARIES := nfd-master nfd-worker nfd-topology-updater nfd-gc kubectl-nfd nfd
9494

9595
build-%:
9696
$(GO_CMD) build -v -o bin/ $(BUILD_FLAGS) ./cmd/$*

cmd/nfd/main.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package main
18+
19+
import (
20+
"sigs.k8s.io/node-feature-discovery/cmd/nfd/subcmd"
21+
)
22+
23+
const ProgramName = "nfd"
24+
25+
func main() {
26+
subcmd.Execute()
27+
}

cmd/nfd/subcmd/compat/compat.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package compat
18+
19+
import (
20+
"fmt"
21+
"os"
22+
23+
"github.com/spf13/cobra"
24+
)
25+
26+
var CompatCmd = &cobra.Command{
27+
Use: "compat",
28+
Short: "Image compatibility commands",
29+
}
30+
31+
func Execute() {
32+
if err := CompatCmd.Execute(); err != nil {
33+
fmt.Println(err)
34+
os.Exit(1)
35+
}
36+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package options
18+
19+
import (
20+
"fmt"
21+
"runtime"
22+
"strings"
23+
24+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
25+
"github.com/spf13/cobra"
26+
)
27+
28+
// PlatformOption represents
29+
type PlatformOption struct {
30+
// PlatformStr contains the raw platform argument provided by the user.
31+
PlatformStr string
32+
// Platform represents the OCI platform specification, built from PlatformStr.
33+
Platform *ocispec.Platform
34+
}
35+
36+
// Parse takes the PlatformStr argument provided by the user
37+
// to build OCI platform specification.
38+
func (opt *PlatformOption) Parse(*cobra.Command) error {
39+
var pStr string
40+
41+
if opt.PlatformStr == "" {
42+
return nil
43+
}
44+
45+
platform := &ocispec.Platform{}
46+
pStr, platform.OSVersion, _ = strings.Cut(opt.PlatformStr, ":")
47+
parts := strings.Split(pStr, "/")
48+
49+
switch len(parts) {
50+
case 3:
51+
platform.Variant = parts[2]
52+
fallthrough
53+
case 2:
54+
platform.Architecture = parts[1]
55+
case 1:
56+
platform.Architecture = runtime.GOARCH
57+
default:
58+
return fmt.Errorf("failed to parse platform %q: expected format os[/arch[/variant]]", opt.PlatformStr)
59+
}
60+
61+
platform.OS = parts[0]
62+
if platform.OS == "" {
63+
return fmt.Errorf("invalid platform: OS cannot be empty")
64+
}
65+
if platform.Architecture == "" {
66+
return fmt.Errorf("invalid platform: Architecture cannot be empty")
67+
}
68+
opt.Platform = platform
69+
return nil
70+
}
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package compat
18+
19+
import (
20+
"context"
21+
"encoding/json"
22+
"fmt"
23+
"io"
24+
"os"
25+
"strings"
26+
"time"
27+
28+
"github.com/jedib0t/go-pretty/v6/table"
29+
"github.com/jedib0t/go-pretty/v6/text"
30+
"github.com/spf13/cobra"
31+
"oras.land/oras-go/v2/registry"
32+
33+
"sigs.k8s.io/node-feature-discovery/cmd/nfd/subcmd/compat/options"
34+
artifactcli "sigs.k8s.io/node-feature-discovery/pkg/client-nfd/compat/artifact-client"
35+
nodevalidator "sigs.k8s.io/node-feature-discovery/pkg/client-nfd/compat/node-validator"
36+
"sigs.k8s.io/node-feature-discovery/source"
37+
)
38+
39+
var (
40+
image string
41+
tags []string
42+
platform options.PlatformOption
43+
plainHTTP bool
44+
outputJSON bool
45+
46+
// secrets
47+
readPassword bool
48+
readAccessToken bool
49+
username string
50+
password string
51+
accessToken string
52+
)
53+
54+
var validateNodeCmd = &cobra.Command{
55+
Use: "validate-node",
56+
Short: "Perform node validation based on its associated image compatibility artifact",
57+
PreRunE: func(cmd *cobra.Command, args []string) error {
58+
var err error
59+
60+
if err = platform.Parse(cmd); err != nil {
61+
return err
62+
}
63+
64+
if readAccessToken && readPassword {
65+
return fmt.Errorf("cannot use --read-access-token and --read-password at the same time")
66+
} else if readAccessToken {
67+
accessToken, err = readStdin()
68+
if err != nil {
69+
return err
70+
}
71+
} else if readPassword {
72+
password, err = readStdin()
73+
if err != nil {
74+
return err
75+
}
76+
}
77+
78+
return nil
79+
},
80+
RunE: func(cmd *cobra.Command, args []string) error {
81+
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
82+
defer cancel()
83+
84+
ref, err := registry.ParseReference(image)
85+
if err != nil {
86+
return err
87+
}
88+
89+
sources := map[string]source.FeatureSource{}
90+
for k, v := range source.GetAllFeatureSources() {
91+
if ts, ok := v.(source.SupplementalSource); ok && ts.DisableByDefault() {
92+
continue
93+
}
94+
sources[k] = v
95+
}
96+
97+
authOpt := artifactcli.WithAuthDefault()
98+
if username != "" && password != "" {
99+
authOpt = artifactcli.WithAuthPassword(username, password)
100+
} else if accessToken != "" {
101+
authOpt = artifactcli.WithAuthToken(accessToken)
102+
}
103+
104+
ac := artifactcli.New(
105+
&ref,
106+
artifactcli.WithArgs(artifactcli.Args{PlainHttp: plainHTTP}),
107+
artifactcli.WithPlatform(platform.Platform),
108+
authOpt,
109+
)
110+
111+
nv := nodevalidator.New(
112+
nodevalidator.WithArgs(&nodevalidator.Args{Tags: tags}),
113+
nodevalidator.WithArtifactClient(ac),
114+
nodevalidator.WithSources(sources),
115+
)
116+
117+
out, err := nv.Execute(ctx)
118+
if err != nil {
119+
return err
120+
}
121+
if outputJSON {
122+
b, err := json.Marshal(out)
123+
if err != nil {
124+
return err
125+
}
126+
fmt.Printf("%s", b)
127+
} else {
128+
pprintResult(out)
129+
}
130+
131+
return nil
132+
},
133+
}
134+
135+
func readStdin() (string, error) {
136+
secretRaw, err := io.ReadAll(os.Stdin)
137+
if err != nil {
138+
return "", err
139+
}
140+
secret := strings.TrimSuffix(string(secretRaw), "\n")
141+
secret = strings.TrimSuffix(secret, "\r")
142+
143+
return secret, nil
144+
}
145+
146+
func pprintResult(css []*nodevalidator.CompatibilityStatus) {
147+
for i, cs := range css {
148+
fmt.Print(text.Colors{text.FgCyan, text.Bold}.Sprintf("COMPATIBILITY SET #%d ", i+1))
149+
fmt.Print(text.FgCyan.Sprintf("Weight: %d", cs.Weight))
150+
if cs.Tag != "" {
151+
fmt.Print(text.FgCyan.Sprintf("; Tag: %s", cs.Tag))
152+
}
153+
fmt.Println()
154+
fmt.Println(text.FgWhite.Sprintf("Description: %s", cs.Description))
155+
156+
for _, r := range cs.Rules {
157+
printTable(r)
158+
}
159+
fmt.Println()
160+
}
161+
}
162+
163+
func printTable(rs nodevalidator.ProcessedRuleStatus) {
164+
t := table.NewWriter()
165+
t.SetStyle(table.StyleLight)
166+
t.SetOutputMirror(os.Stdout)
167+
t.Style().Format.Header = text.FormatDefault
168+
t.SetAutoIndex(true)
169+
170+
validTxt := text.BgRed.Sprint(" FAIL ")
171+
if rs.IsMatch {
172+
validTxt = text.BgGreen.Sprint(" OK ")
173+
}
174+
ruleTxt := strings.ToUpper(fmt.Sprintf("rule: %s", rs.Name))
175+
176+
t.SetTitle(text.Bold.Sprintf("%s - %s", ruleTxt, validTxt))
177+
t.AppendHeader(table.Row{"Feature", "Expression", "Matcher Type", "Status"})
178+
179+
if mf := rs.MatchedExpressions; len(mf) > 0 {
180+
renderMatchFeatures(t, mf)
181+
}
182+
if ma := rs.MatchedAny; len(ma) > 0 {
183+
for _, elem := range ma {
184+
t.AppendSeparator()
185+
renderMatchFeatures(t, elem.MatchedExpressions)
186+
}
187+
}
188+
t.Render()
189+
}
190+
191+
func renderMatchFeatures(t table.Writer, matchedExpressions []nodevalidator.MatchedExpression) {
192+
for _, fm := range matchedExpressions {
193+
fullFeatureDomain := fm.Feature
194+
if fm.Name != "" {
195+
fullFeatureDomain = fmt.Sprintf("%s.%s", fm.Feature, fm.Name)
196+
}
197+
198+
addTableRows(t, fullFeatureDomain, fm.Expression.String(), fm.MatcherType, fm.IsMatch)
199+
}
200+
}
201+
202+
func addTableRows(t table.Writer, fullFeatureDomain, expression string, matcherType nodevalidator.MatcherType, isMatch bool) {
203+
status := text.FgHiRed.Sprint("FAIL")
204+
if isMatch {
205+
status = text.FgHiGreen.Sprint("OK")
206+
}
207+
t.AppendRow(table.Row{fullFeatureDomain, expression, matcherType, status})
208+
}
209+
210+
func init() {
211+
CompatCmd.AddCommand(validateNodeCmd)
212+
validateNodeCmd.Flags().StringVar(&image, "image", "", "the URL of the image containing compatibility metadata")
213+
validateNodeCmd.Flags().StringSliceVar(&tags, "tags", []string{}, "a list of tags that must match the tags set on the compatibility objects")
214+
validateNodeCmd.Flags().StringVar(&platform.PlatformStr, "platform", "", "the artifact platform in the format os[/arch][/variant][:os_version]")
215+
validateNodeCmd.Flags().BoolVar(&plainHTTP, "plain-http", false, "use of HTTP protocol for all registry communications")
216+
validateNodeCmd.Flags().BoolVar(&outputJSON, "output-json", false, "print a JSON object")
217+
validateNodeCmd.Flags().StringVar(&username, "reg-username", "", "registry username")
218+
validateNodeCmd.Flags().BoolVar(&readPassword, "reg-password-stdin", false, "read registry password from stdin")
219+
validateNodeCmd.Flags().BoolVar(&readAccessToken, "reg-token-stdin", false, "read registry access token from stdin")
220+
221+
if err := validateNodeCmd.MarkFlagRequired("image"); err != nil {
222+
panic(err)
223+
}
224+
}

0 commit comments

Comments
 (0)