Skip to content

Commit b62e7c1

Browse files
authored
Merge pull request #2038 from SonicInfra/sprig-template
Use Sprig template functions and add asLabelValue function
2 parents 797c66f + b0dbd11 commit b62e7c1

File tree

6 files changed

+115
-57
lines changed

6 files changed

+115
-57
lines changed

docs/usage/customization-guide.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,14 +1100,17 @@ these separate expansions would be created, i.e. the end result would be a
11001100
union of all the individual expansions.
11011101

11021102
Rule templates use the Golang [text/template](https://pkg.go.dev/text/template)
1103-
package and all its built-in functionality (e.g. pipelines and functions) can
1103+
package along with [Sprig functions](https://masterminds.github.io/sprig/)
1104+
and all their functionality (e.g. pipelines and functions) can
11041105
be used. An example template taking use of the built-in `len` function,
1105-
advertising the number of PCI network controllers from a specific vendor:
1106+
advertising the number of PCI network controllers from a specific vendor,
1107+
and using Sprig's `first`, `trim` and `substr` to advertise the first one's class:
11061108
<!-- {% raw %} -->
11071109

11081110
```yaml
11091111
labelsTemplate: |
11101112
num-intel-network-controllers={{ .pci.device | len }}
1113+
first-intel-network-controllers={{ (.pci.device | first).class | trim | substr 0 63 }}
11111114
matchFeatures:
11121115
- feature: pci.device
11131116
matchExpressions:
@@ -1117,6 +1120,7 @@ advertising the number of PCI network controllers from a specific vendor:
11171120
```
11181121

11191122
<!-- {% endraw %} -->
1123+
11201124
Imaginative template pipelines are possible, but care must be taken to
11211125
produce understandable and maintainable rule sets.
11221126

go.mod

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module sigs.k8s.io/node-feature-discovery
33
go 1.24
44

55
require (
6+
github.com/Masterminds/sprig/v3 v3.3.0
67
github.com/fsnotify/fsnotify v1.8.0
78
github.com/google/go-cmp v0.6.0
89
github.com/google/uuid v1.6.0
@@ -42,7 +43,10 @@ require (
4243

4344
require (
4445
cel.dev/expr v0.19.0 // indirect
46+
dario.cat/mergo v1.0.1 // indirect
4547
github.com/JeffAshton/win_pdh v0.0.0-20161109143554-76bb4ee9f0ab // indirect
48+
github.com/Masterminds/goutils v1.1.1 // indirect
49+
github.com/Masterminds/semver/v3 v3.3.0 // indirect
4650
github.com/Microsoft/go-winio v0.6.2 // indirect
4751
github.com/NYTimes/gziphandler v1.1.1 // indirect
4852
github.com/OneOfOne/xxhash v1.2.8 // indirect
@@ -87,6 +91,7 @@ require (
8791
github.com/gorilla/websocket v1.5.0 // indirect
8892
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
8993
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
94+
github.com/huandu/xstrings v1.5.0 // indirect
9095
github.com/inconshreveable/mousetrap v1.1.0 // indirect
9196
github.com/jaypipes/pcidb v1.0.1 // indirect
9297
github.com/josharian/intern v1.0.0 // indirect
@@ -96,7 +101,9 @@ require (
96101
github.com/mailru/easyjson v0.7.7 // indirect
97102
github.com/mattn/go-runewidth v0.0.16 // indirect
98103
github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible // indirect
104+
github.com/mitchellh/copystructure v1.2.0 // indirect
99105
github.com/mitchellh/go-homedir v1.1.0 // indirect
106+
github.com/mitchellh/reflectwalk v1.0.2 // indirect
100107
github.com/moby/spdystream v0.5.0 // indirect
101108
github.com/moby/sys/mountinfo v0.7.2 // indirect
102109
github.com/moby/sys/userns v0.1.0 // indirect
@@ -113,8 +120,10 @@ require (
113120
github.com/prometheus/common v0.55.0 // indirect
114121
github.com/prometheus/procfs v0.15.1 // indirect
115122
github.com/rivo/uniseg v0.4.7 // indirect
123+
github.com/shopspring/decimal v1.4.0 // indirect
116124
github.com/sirupsen/logrus v1.9.3 // indirect
117125
github.com/smarty/assertions v1.15.1 // indirect
126+
github.com/spf13/cast v1.7.0 // indirect
118127
github.com/spf13/pflag v1.0.6 // indirect
119128
github.com/stoewer/go-strcase v1.3.0 // indirect
120129
github.com/stretchr/objx v0.5.2 // indirect

go.sum

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
cel.dev/expr v0.19.0 h1:lXuo+nDhpyJSpWxpPVi5cPUwzKb+dsdOiw6IreM5yt0=
22
cel.dev/expr v0.19.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
3+
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
4+
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
35
github.com/JeffAshton/win_pdh v0.0.0-20161109143554-76bb4ee9f0ab h1:UKkYhof1njT1/xq4SEg5z+VpTgjmNeHwPGRQl7takDI=
46
github.com/JeffAshton/win_pdh v0.0.0-20161109143554-76bb4ee9f0ab/go.mod h1:3VYc5hodBMJ5+l/7J4xAyMeuM2PNuepvHlGs8yilUCA=
7+
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
8+
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
9+
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
10+
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
11+
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
12+
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
513
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
614
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
715
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
@@ -62,6 +70,8 @@ github.com/euank/go-kmsg-parser v2.0.0+incompatible h1:cHD53+PLQuuQyLZeriD1V/esu
6270
github.com/euank/go-kmsg-parser v2.0.0+incompatible/go.mod h1:MhmAMZ8V4CYH4ybgdRwPr2TU5ThnS43puaKEMpja1uw=
6371
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
6472
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
73+
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
74+
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
6575
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
6676
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
6777
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
@@ -125,6 +135,8 @@ github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4
125135
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
126136
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
127137
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
138+
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
139+
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
128140
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
129141
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
130142
github.com/jaypipes/ghw v0.13.0 h1:log8MXuB8hzTNnSktqpXMHc0c/2k/WgjOMSUtnI1RV4=
@@ -165,8 +177,12 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
165177
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
166178
github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible h1:aKW/4cBs+yK6gpqU3K/oIwk9Q/XICqd3zOX/UFuvqmk=
167179
github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=
180+
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
181+
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
168182
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
169183
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
184+
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
185+
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
170186
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
171187
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
172188
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
@@ -217,6 +233,8 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
217233
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
218234
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
219235
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
236+
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
237+
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
220238
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
221239
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
222240
github.com/smarty/assertions v1.15.1 h1:812oFiXI+G55vxsFf+8bIZ1ux30qtkdqzKbEFwyX3Tk=
@@ -225,6 +243,8 @@ github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sS
225243
github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
226244
github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js=
227245
github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
246+
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
247+
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
228248
github.com/spf13/cobra v1.9.0 h1:Py5fIuq/lJsRYxcxfOtsJqpmwJWCMOUy2tMJYV8TNHE=
229249
github.com/spf13/cobra v1.9.0/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
230250
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=

pkg/apis/nfd/nodefeaturerule/rule.go

Lines changed: 5 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,16 @@ limitations under the License.
1717
package nodefeaturerule
1818

1919
import (
20-
"bytes"
2120
"fmt"
2221
"maps"
2322
"slices"
2423
"strings"
25-
"text/template"
2624

2725
corev1 "k8s.io/api/core/v1"
2826
"k8s.io/klog/v2"
2927

3028
nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/api/nfd/v1alpha1"
29+
"sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/template"
3130
"sigs.k8s.io/node-feature-discovery/pkg/utils"
3231
)
3332

@@ -188,12 +187,12 @@ func executeLabelsTemplate(r *nfdv1alpha1.Rule, in matchedFeatures, out map[stri
188187
return nil
189188
}
190189

191-
th, err := newTemplateHelper(r.LabelsTemplate)
190+
th, err := template.NewHelper(r.LabelsTemplate)
192191
if err != nil {
193192
return fmt.Errorf("failed to parse LabelsTemplate: %w", err)
194193
}
195194

196-
labels, err := th.expandMap(in)
195+
labels, err := th.ExpandMap(in)
197196
if err != nil {
198197
return fmt.Errorf("failed to expand LabelsTemplate: %w", err)
199198
}
@@ -208,12 +207,12 @@ func executeVarsTemplate(r *nfdv1alpha1.Rule, in matchedFeatures, out map[string
208207
return nil
209208
}
210209

211-
th, err := newTemplateHelper(r.VarsTemplate)
210+
th, err := template.NewHelper(r.VarsTemplate)
212211
if err != nil {
213212
return err
214213
}
215214

216-
vars, err := th.expandMap(in)
215+
vars, err := th.ExpandMap(in)
217216
if err != nil {
218217
return err
219218
}
@@ -309,47 +308,3 @@ func evaluateFeatureMatcher(m *nfdv1alpha1.FeatureMatcher, features *nfdv1alpha1
309308
}
310309
return isMatch, status, nil
311310
}
312-
313-
type templateHelper struct {
314-
template *template.Template
315-
}
316-
317-
func newTemplateHelper(name string) (*templateHelper, error) {
318-
tmpl, err := template.New("").Option("missingkey=error").Parse(name)
319-
if err != nil {
320-
return nil, fmt.Errorf("invalid template: %w", err)
321-
}
322-
return &templateHelper{template: tmpl}, nil
323-
}
324-
325-
func (h *templateHelper) execute(data interface{}) (string, error) {
326-
var tmp bytes.Buffer
327-
if err := h.template.Execute(&tmp, data); err != nil {
328-
return "", err
329-
}
330-
return tmp.String(), nil
331-
}
332-
333-
// expandMap is a helper for expanding a template in to a map of strings. Data
334-
// after executing the template is expexted to be key=value pairs separated by
335-
// newlines.
336-
func (h *templateHelper) expandMap(data interface{}) (map[string]string, error) {
337-
expanded, err := h.execute(data)
338-
if err != nil {
339-
return nil, err
340-
}
341-
342-
// Split out individual key-value pairs
343-
out := make(map[string]string)
344-
for _, item := range strings.Split(expanded, "\n") {
345-
// Remove leading/trailing whitespace and skip empty lines
346-
if trimmed := strings.TrimSpace(item); trimmed != "" {
347-
split := strings.SplitN(trimmed, "=", 2)
348-
if len(split) == 1 {
349-
return nil, fmt.Errorf("missing value in expanded template line %q, (format must be '<key>=<value>')", trimmed)
350-
}
351-
out[split[0]] = split[1]
352-
}
353-
}
354-
return out, nil
355-
}

pkg/apis/nfd/template/template.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
Copyright 2025 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 template
18+
19+
import (
20+
"bytes"
21+
"fmt"
22+
"strings"
23+
"text/template"
24+
25+
"github.com/Masterminds/sprig/v3"
26+
)
27+
28+
type Helper struct {
29+
template *template.Template
30+
}
31+
32+
func NewHelper(name string) (*Helper, error) {
33+
tmpl := template.New("").Funcs(sprig.FuncMap()).Option("missingkey=error")
34+
tmpl, err := tmpl.Parse(name)
35+
if err != nil {
36+
return nil, fmt.Errorf("invalid template: %w", err)
37+
}
38+
return &Helper{template: tmpl}, nil
39+
}
40+
41+
func (h *Helper) execute(data interface{}) (string, error) {
42+
var tmp bytes.Buffer
43+
if err := h.template.Execute(&tmp, data); err != nil {
44+
return "", err
45+
}
46+
return tmp.String(), nil
47+
}
48+
49+
// ExpandMap is a helper for expanding a template in to a map of strings. Data
50+
// after executing the template is expected to be key=value pairs separated by
51+
// newlines.
52+
func (h *Helper) ExpandMap(data interface{}) (map[string]string, error) {
53+
expanded, err := h.execute(data)
54+
if err != nil {
55+
return nil, err
56+
}
57+
58+
// Split out individual key-value pairs
59+
out := make(map[string]string)
60+
for _, item := range strings.Split(expanded, "\n") {
61+
// Remove leading/trailing whitespace and skip empty lines
62+
if trimmed := strings.TrimSpace(item); trimmed != "" {
63+
split := strings.SplitN(trimmed, "=", 2)
64+
if len(split) == 1 {
65+
return nil, fmt.Errorf("missing value in expanded template line %q, (format must be '<key>=<value>')", trimmed)
66+
}
67+
out[split[0]] = split[1]
68+
}
69+
}
70+
return out, nil
71+
}

pkg/apis/nfd/validate/validate.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ package validate
1919
import (
2020
"fmt"
2121
"strings"
22-
"text/template"
2322

2423
corev1 "k8s.io/api/core/v1"
2524
k8sQuantity "k8s.io/apimachinery/pkg/api/resource"
2625
k8svalidation "k8s.io/apimachinery/pkg/util/validation"
2726

2827
nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/api/nfd/v1alpha1"
28+
"sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/template"
2929
)
3030

3131
var (
@@ -70,11 +70,10 @@ func MatchFeatures(matchFeature nfdv1alpha1.FeatureMatcher) []error {
7070
// template is invalid.
7171
func Template(labelsTemplate string) []error {
7272
var validationErr []error
73-
74-
// Validate template
75-
_, err := template.New("").Option("missingkey=error").Parse(labelsTemplate)
73+
// Only validate template
74+
_, err := template.NewHelper(labelsTemplate)
7675
if err != nil {
77-
validationErr = append(validationErr, fmt.Errorf("invalid template: %w", err))
76+
validationErr = append(validationErr, err)
7877
}
7978
return validationErr
8079
}

0 commit comments

Comments
 (0)