From b0dbd11af718b50d54cb0776d9a87521844f6887 Mon Sep 17 00:00:00 2001 From: Fahrzin Hemmati Date: Wed, 29 Jan 2025 15:12:39 -0800 Subject: [PATCH] Use Sprig template functions and add asLabelValue function --- docs/usage/customization-guide.md | 8 +++- go.mod | 9 ++++ go.sum | 20 ++++++++ pkg/apis/nfd/nodefeaturerule/rule.go | 55 ++------------------- pkg/apis/nfd/template/template.go | 71 ++++++++++++++++++++++++++++ pkg/apis/nfd/validate/validate.go | 9 ++-- 6 files changed, 115 insertions(+), 57 deletions(-) create mode 100644 pkg/apis/nfd/template/template.go diff --git a/docs/usage/customization-guide.md b/docs/usage/customization-guide.md index 3fca6578e..aa0b9ff80 100644 --- a/docs/usage/customization-guide.md +++ b/docs/usage/customization-guide.md @@ -1100,14 +1100,17 @@ these separate expansions would be created, i.e. the end result would be a union of all the individual expansions. Rule templates use the Golang [text/template](https://pkg.go.dev/text/template) -package and all its built-in functionality (e.g. pipelines and functions) can +package along with [Sprig functions](https://masterminds.github.io/sprig/) +and all their functionality (e.g. pipelines and functions) can be used. An example template taking use of the built-in `len` function, -advertising the number of PCI network controllers from a specific vendor: +advertising the number of PCI network controllers from a specific vendor, +and using Sprig's `first`, `trim` and `substr` to advertise the first one's class: ```yaml labelsTemplate: | num-intel-network-controllers={{ .pci.device | len }} + first-intel-network-controllers={{ (.pci.device | first).class | trim | substr 0 63 }} matchFeatures: - feature: pci.device matchExpressions: @@ -1117,6 +1120,7 @@ advertising the number of PCI network controllers from a specific vendor: ``` + Imaginative template pipelines are possible, but care must be taken to produce understandable and maintainable rule sets. diff --git a/go.mod b/go.mod index f2f0934d6..62bf056da 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module sigs.k8s.io/node-feature-discovery go 1.24 require ( + github.com/Masterminds/sprig/v3 v3.3.0 github.com/fsnotify/fsnotify v1.8.0 github.com/google/go-cmp v0.6.0 github.com/google/uuid v1.6.0 @@ -42,7 +43,10 @@ require ( require ( cel.dev/expr v0.19.0 // indirect + dario.cat/mergo v1.0.1 // indirect github.com/JeffAshton/win_pdh v0.0.0-20161109143554-76bb4ee9f0ab // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.3.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect github.com/OneOfOne/xxhash v1.2.8 // indirect @@ -87,6 +91,7 @@ require ( github.com/gorilla/websocket v1.5.0 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect + github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jaypipes/pcidb v1.0.1 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -96,7 +101,9 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/spdystream v0.5.0 // indirect github.com/moby/sys/mountinfo v0.7.2 // indirect github.com/moby/sys/userns v0.1.0 // indirect @@ -113,8 +120,10 @@ require ( github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/smarty/assertions v1.15.1 // indirect + github.com/spf13/cast v1.7.0 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect github.com/stretchr/objx v0.5.2 // indirect diff --git a/go.sum b/go.sum index abc70c758..560efc757 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,15 @@ cel.dev/expr v0.19.0 h1:lXuo+nDhpyJSpWxpPVi5cPUwzKb+dsdOiw6IreM5yt0= cel.dev/expr v0.19.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/JeffAshton/win_pdh v0.0.0-20161109143554-76bb4ee9f0ab h1:UKkYhof1njT1/xq4SEg5z+VpTgjmNeHwPGRQl7takDI= github.com/JeffAshton/win_pdh v0.0.0-20161109143554-76bb4ee9f0ab/go.mod h1:3VYc5hodBMJ5+l/7J4xAyMeuM2PNuepvHlGs8yilUCA= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 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 github.com/euank/go-kmsg-parser v2.0.0+incompatible/go.mod h1:MhmAMZ8V4CYH4ybgdRwPr2TU5ThnS43puaKEMpja1uw= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 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 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 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 github.com/mattn/go-runewidth v0.0.16/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/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 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 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= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smarty/assertions v1.15.1 h1:812oFiXI+G55vxsFf+8bIZ1ux30qtkdqzKbEFwyX3Tk= @@ -225,6 +243,8 @@ github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sS github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.9.0 h1:Py5fIuq/lJsRYxcxfOtsJqpmwJWCMOUy2tMJYV8TNHE= github.com/spf13/cobra v1.9.0/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= diff --git a/pkg/apis/nfd/nodefeaturerule/rule.go b/pkg/apis/nfd/nodefeaturerule/rule.go index 9289eaa12..fffb6ca16 100644 --- a/pkg/apis/nfd/nodefeaturerule/rule.go +++ b/pkg/apis/nfd/nodefeaturerule/rule.go @@ -17,17 +17,16 @@ limitations under the License. package nodefeaturerule import ( - "bytes" "fmt" "maps" "slices" "strings" - "text/template" corev1 "k8s.io/api/core/v1" "k8s.io/klog/v2" nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/api/nfd/v1alpha1" + "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/template" "sigs.k8s.io/node-feature-discovery/pkg/utils" ) @@ -188,12 +187,12 @@ func executeLabelsTemplate(r *nfdv1alpha1.Rule, in matchedFeatures, out map[stri return nil } - th, err := newTemplateHelper(r.LabelsTemplate) + th, err := template.NewHelper(r.LabelsTemplate) if err != nil { return fmt.Errorf("failed to parse LabelsTemplate: %w", err) } - labels, err := th.expandMap(in) + labels, err := th.ExpandMap(in) if err != nil { return fmt.Errorf("failed to expand LabelsTemplate: %w", err) } @@ -208,12 +207,12 @@ func executeVarsTemplate(r *nfdv1alpha1.Rule, in matchedFeatures, out map[string return nil } - th, err := newTemplateHelper(r.VarsTemplate) + th, err := template.NewHelper(r.VarsTemplate) if err != nil { return err } - vars, err := th.expandMap(in) + vars, err := th.ExpandMap(in) if err != nil { return err } @@ -309,47 +308,3 @@ func evaluateFeatureMatcher(m *nfdv1alpha1.FeatureMatcher, features *nfdv1alpha1 } return isMatch, status, nil } - -type templateHelper struct { - template *template.Template -} - -func newTemplateHelper(name string) (*templateHelper, error) { - tmpl, err := template.New("").Option("missingkey=error").Parse(name) - if err != nil { - return nil, fmt.Errorf("invalid template: %w", err) - } - return &templateHelper{template: tmpl}, nil -} - -func (h *templateHelper) execute(data interface{}) (string, error) { - var tmp bytes.Buffer - if err := h.template.Execute(&tmp, data); err != nil { - return "", err - } - return tmp.String(), nil -} - -// expandMap is a helper for expanding a template in to a map of strings. Data -// after executing the template is expexted to be key=value pairs separated by -// newlines. -func (h *templateHelper) expandMap(data interface{}) (map[string]string, error) { - expanded, err := h.execute(data) - if err != nil { - return nil, err - } - - // Split out individual key-value pairs - out := make(map[string]string) - for _, item := range strings.Split(expanded, "\n") { - // Remove leading/trailing whitespace and skip empty lines - if trimmed := strings.TrimSpace(item); trimmed != "" { - split := strings.SplitN(trimmed, "=", 2) - if len(split) == 1 { - return nil, fmt.Errorf("missing value in expanded template line %q, (format must be '=')", trimmed) - } - out[split[0]] = split[1] - } - } - return out, nil -} diff --git a/pkg/apis/nfd/template/template.go b/pkg/apis/nfd/template/template.go new file mode 100644 index 000000000..4c0242495 --- /dev/null +++ b/pkg/apis/nfd/template/template.go @@ -0,0 +1,71 @@ +/* +Copyright 2025 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 template + +import ( + "bytes" + "fmt" + "strings" + "text/template" + + "github.com/Masterminds/sprig/v3" +) + +type Helper struct { + template *template.Template +} + +func NewHelper(name string) (*Helper, error) { + tmpl := template.New("").Funcs(sprig.FuncMap()).Option("missingkey=error") + tmpl, err := tmpl.Parse(name) + if err != nil { + return nil, fmt.Errorf("invalid template: %w", err) + } + return &Helper{template: tmpl}, nil +} + +func (h *Helper) execute(data interface{}) (string, error) { + var tmp bytes.Buffer + if err := h.template.Execute(&tmp, data); err != nil { + return "", err + } + return tmp.String(), nil +} + +// ExpandMap is a helper for expanding a template in to a map of strings. Data +// after executing the template is expected to be key=value pairs separated by +// newlines. +func (h *Helper) ExpandMap(data interface{}) (map[string]string, error) { + expanded, err := h.execute(data) + if err != nil { + return nil, err + } + + // Split out individual key-value pairs + out := make(map[string]string) + for _, item := range strings.Split(expanded, "\n") { + // Remove leading/trailing whitespace and skip empty lines + if trimmed := strings.TrimSpace(item); trimmed != "" { + split := strings.SplitN(trimmed, "=", 2) + if len(split) == 1 { + return nil, fmt.Errorf("missing value in expanded template line %q, (format must be '=')", trimmed) + } + out[split[0]] = split[1] + } + } + return out, nil +} diff --git a/pkg/apis/nfd/validate/validate.go b/pkg/apis/nfd/validate/validate.go index daa65bb02..56dc0e1e1 100644 --- a/pkg/apis/nfd/validate/validate.go +++ b/pkg/apis/nfd/validate/validate.go @@ -19,13 +19,13 @@ package validate import ( "fmt" "strings" - "text/template" corev1 "k8s.io/api/core/v1" k8sQuantity "k8s.io/apimachinery/pkg/api/resource" k8svalidation "k8s.io/apimachinery/pkg/util/validation" nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/api/nfd/v1alpha1" + "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/template" ) var ( @@ -70,11 +70,10 @@ func MatchFeatures(matchFeature nfdv1alpha1.FeatureMatcher) []error { // template is invalid. func Template(labelsTemplate string) []error { var validationErr []error - - // Validate template - _, err := template.New("").Option("missingkey=error").Parse(labelsTemplate) + // Only validate template + _, err := template.NewHelper(labelsTemplate) if err != nil { - validationErr = append(validationErr, fmt.Errorf("invalid template: %w", err)) + validationErr = append(validationErr, err) } return validationErr }