Skip to content

Commit aab4eb5

Browse files
committed
feat: Use dlclark/regexp2 over standard library's package
Signed-off-by: Pranshu Srivastava <[email protected]>
1 parent bc60722 commit aab4eb5

File tree

6 files changed

+128
-28
lines changed

6 files changed

+128
-28
lines changed

docs/developer/cli-arguments.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ Flags:
5858
--logtostderr log to standard error instead of files (default true)
5959
--metric-allowlist string Comma-separated list of metrics to be exposed. This list comprises of exact metric names and/or regex patterns. The allowlist and denylist are mutually exclusive.
6060
--metric-annotations-allowlist string Comma-separated list of Kubernetes annotations keys that will be used in the resource' labels metric. By default the annotations metrics are not exposed. To include them, provide a list of resource names in their plural form and Kubernetes annotation keys you would like to allow for them (Example: '=namespaces=[kubernetes.io/team,...],pods=[kubernetes.io/team],...)'. A single '*' can be provided per resource instead to allow any annotations, but that has severe performance implications (Example: '=pods=[*]').
61-
--metric-denylist string Comma-separated list of metrics not to be enabled. This list comprises of exact metric names and/or regex patterns. The allowlist and denylist are mutually exclusive.
61+
--metric-denylist string Comma-separated list of metrics not to be enabled. This list comprises of exact metric names and/or *ECMAScript-based* regex patterns. The allowlist and denylist are mutually exclusive.
6262
--metric-labels-allowlist string Comma-separated list of additional Kubernetes label keys that will be used in the resource' labels metric. By default the labels metrics are not exposed. To include them, provide a list of resource names in their plural form and Kubernetes label keys you would like to allow for them (Example: '=namespaces=[k8s-label-1,k8s-label-n,...],pods=[app],...)'. A single '*' can be provided per resource instead to allow any labels, but that has severe performance implications (Example: '=pods=[*]'). Additionally, an asterisk (*) can be provided as a key, which will resolve to all resources, i.e., assuming '--resources=deployments,pods', '=*=[*]' will resolve to '=deployments=[*],pods=[*]'.
6363
--metric-opt-in-list string Comma-separated list of metrics which are opt-in and not enabled by default. This is in addition to the metric allow- and denylists
6464
--namespaces string Comma-separated list of namespaces to be enabled. Defaults to ""

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.24.0
55
require (
66
github.com/KimMachineGun/automemlimit v0.7.1
77
github.com/dgryski/go-jump v0.0.0-20211018200510-ba001c3ffce0
8+
github.com/dlclark/regexp2 v1.11.5
89
github.com/fsnotify/fsnotify v1.8.0
910
github.com/go-logr/logr v1.4.2
1011
github.com/gobuffalo/flect v1.0.3

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,8 @@ github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcej
188188
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
189189
github.com/elliotchance/orderedmap/v2 v2.2.0 h1:7/2iwO98kYT4XkOjA9mBEIwvi4KpGB4cyHeOFOnj4Vk=
190190
github.com/elliotchance/orderedmap/v2 v2.2.0/go.mod h1:85lZyVbpGaGvHvnKa7Qhx7zncAdBIBq6u56Hb1PRU5Q=
191+
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
192+
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
191193
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
192194
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
193195
github.com/emicklei/proto v1.13.2 h1:z/etSFO3uyXeuEsVPzfl56WNgzcvIr42aQazXaQmFZY=

pkg/allowdenylist/allowdenylist.go

+38-11
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,22 @@ package allowdenylist
1818

1919
import (
2020
"errors"
21-
"regexp"
2221
"strings"
22+
"time"
23+
24+
regexp "github.com/dlclark/regexp2"
25+
"k8s.io/klog/v2"
2326

2427
generator "k8s.io/kube-state-metrics/v2/pkg/metric_generator"
2528
)
2629

27-
// AllowDenyList encapsulates the logic needed to filter based on a string.
30+
// Use ECMAScript as the default regexp spec to support lookarounds (#2594).
31+
var (
32+
regexpDefaultSpec regexp.RegexOptions = regexp.ECMAScript
33+
regexpDefaultTimeout = time.Minute
34+
)
35+
36+
// AllowDenyList namespaceencapsulates the logic needed to filter based on a string.
2837
type AllowDenyList struct {
2938
list map[string]struct{}
3039
rList []*regexp.Regexp
@@ -34,6 +43,7 @@ type AllowDenyList struct {
3443
// New constructs a new AllowDenyList based on a allow- and a
3544
// denylist. Only one of them can be not empty.
3645
func New(allow, deny map[string]struct{}) (*AllowDenyList, error) {
46+
regexp.DefaultMatchTimeout = regexpDefaultTimeout
3747
if len(allow) != 0 && len(deny) != 0 {
3848
return nil, errors.New(
3949
"allowlist and denylist are both set, they are mutually exclusive, only one of them can be set",
@@ -62,7 +72,7 @@ func New(allow, deny map[string]struct{}) (*AllowDenyList, error) {
6272
func (l *AllowDenyList) Parse() error {
6373
regexes := make([]*regexp.Regexp, 0, len(l.list))
6474
for item := range l.list {
65-
r, err := regexp.Compile(item)
75+
r, err := regexp.Compile(item, regexpDefaultSpec)
6676
if err != nil {
6777
return err
6878
}
@@ -99,25 +109,36 @@ func (l *AllowDenyList) Exclude(items []string) {
99109
}
100110

101111
// IsIncluded returns if the given item is included.
102-
func (l *AllowDenyList) IsIncluded(item string) bool {
103-
var matched bool
112+
func (l *AllowDenyList) IsIncluded(item string) (bool, error) {
113+
var (
114+
matched bool
115+
err error
116+
)
104117
for _, r := range l.rList {
105-
matched = r.MatchString(item)
118+
matched, err = r.MatchString(item)
119+
if err != nil {
120+
return false, err
121+
}
106122
if matched {
107123
break
108124
}
109125
}
110126

111127
if l.isAllowList {
112-
return matched
128+
return matched, nil
113129
}
114130

115-
return !matched
131+
return !matched, nil
116132
}
117133

118134
// IsExcluded returns if the given item is excluded.
119-
func (l *AllowDenyList) IsExcluded(item string) bool {
120-
return !l.IsIncluded(item)
135+
func (l *AllowDenyList) IsExcluded(item string) (bool, error) {
136+
isIncluded, err := l.IsIncluded(item)
137+
if err != nil {
138+
return false, err
139+
}
140+
141+
return !isIncluded, nil
121142
}
122143

123144
// Status returns the status of the AllowDenyList that can e.g. be passed into
@@ -137,7 +158,13 @@ func (l *AllowDenyList) Status() string {
137158

138159
// Test returns if the given family generator passes (is included in) the AllowDenyList
139160
func (l *AllowDenyList) Test(generator generator.FamilyGenerator) bool {
140-
return l.IsIncluded(generator.Name)
161+
isIncluded, err := l.IsIncluded(generator.Name)
162+
if err != nil {
163+
klog.ErrorS(err, "Error while processing allow-deny entries for generator", "generator", generator.Name)
164+
return false
165+
}
166+
167+
return isIncluded
141168
}
142169

143170
func copyList(l map[string]struct{}) map[string]struct{} {

pkg/allowdenylist/allowdenylist_test.go

+84-14
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@ limitations under the License.
1717
package allowdenylist
1818

1919
import (
20-
"regexp"
20+
"fmt"
21+
"strings"
2122
"testing"
23+
"time"
24+
25+
regexp "github.com/dlclark/regexp2"
2226
)
2327

2428
func TestNew(t *testing.T) {
@@ -76,7 +80,11 @@ func TestInclude(t *testing.T) {
7680
t.Fatal("expected Parse() to not fail")
7781
}
7882

79-
if !allowlist.IsIncluded("item1") {
83+
isIncluded, err := allowlist.IsIncluded("item1")
84+
if err != nil {
85+
t.Fatal("expected IsIncluded() to not fail")
86+
}
87+
if !isIncluded {
8088
t.Fatal("expected included item to be included")
8189
}
8290
})
@@ -93,7 +101,11 @@ func TestInclude(t *testing.T) {
93101
t.Fatalf("expected Parse() to not fail, but got error : %v", err)
94102
}
95103

96-
if !denylist.IsIncluded(item1) {
104+
isIncluded, err := denylist.IsIncluded(item1)
105+
if err != nil {
106+
t.Fatal("expected IsIncluded() to not fail")
107+
}
108+
if !isIncluded {
97109
t.Fatal("expected included item to be included")
98110
}
99111
})
@@ -103,13 +115,17 @@ func TestInclude(t *testing.T) {
103115
t.Fatal("expected New() to not fail")
104116
}
105117

106-
allowlist.Include([]string{"kube_.*_info"})
118+
allowlist.Include([]string{"kube_(?=secret).*_info"})
107119
err = allowlist.Parse()
108120
if err != nil {
109121
t.Fatalf("expected Parse() to not fail, but got error : %v", err)
110122
}
111123

112-
if !allowlist.IsIncluded("kube_secret_info") {
124+
isIncluded, err := allowlist.IsIncluded("kube_secret_info")
125+
if err != nil {
126+
t.Fatal("expected IsIncluded() to not fail")
127+
}
128+
if !isIncluded {
113129
t.Fatal("expected included item to be included")
114130
}
115131
})
@@ -124,22 +140,38 @@ func TestInclude(t *testing.T) {
124140
t.Fatal("expected New() to not fail")
125141
}
126142

127-
denylist.Exclude([]string{"kube_node_.*_cores", "kube_pod_.*_bytes"})
143+
denylist.Exclude([]string{"kube_(?=node.*cores|pod.*bytes)"})
128144
err = denylist.Parse()
129145
if err != nil {
130146
t.Fatalf("expected Parse() to not fail, but got error : %v", err)
131147
}
132148

133-
if denylist.IsExcluded(item1) {
149+
isExcluded, err := denylist.IsExcluded(item1)
150+
if err != nil {
151+
t.Fatal("expected IsExcluded() to not fail")
152+
}
153+
if isExcluded {
134154
t.Fatalf("expected included %s to be included", item1)
135155
}
136-
if denylist.IsIncluded(item2) {
156+
isIncluded, err := denylist.IsIncluded(item2)
157+
if err != nil {
158+
t.Fatal("expected IsIncluded() to not fail")
159+
}
160+
if isIncluded {
137161
t.Fatalf("expected included %s to be excluded", item2)
138162
}
139-
if denylist.IsIncluded(item3) {
163+
isIncluded, err = denylist.IsIncluded(item3)
164+
if err != nil {
165+
t.Fatal("expected IsIncluded() to not fail")
166+
}
167+
if isIncluded {
140168
t.Fatalf("expected included %s to be excluded", item3)
141169
}
142-
if denylist.IsExcluded(item4) {
170+
isExcluded, err = denylist.IsExcluded(item4)
171+
if err != nil {
172+
t.Fatal("expected IsExcluded() to not fail")
173+
}
174+
if isExcluded {
143175
t.Fatalf("expected included %s to be included", item4)
144176
}
145177
})
@@ -159,7 +191,11 @@ func TestExclude(t *testing.T) {
159191
t.Fatalf("expected Parse() to not fail, but got error : %v", err)
160192
}
161193

162-
if allowlist.IsIncluded(item1) {
194+
isIncluded, err := allowlist.IsIncluded(item1)
195+
if err != nil {
196+
t.Fatal("expected IsIncluded() to not fail")
197+
}
198+
if isIncluded {
163199
t.Fatal("expected excluded item to be excluded")
164200
}
165201
})
@@ -176,7 +212,11 @@ func TestExclude(t *testing.T) {
176212
t.Fatalf("expected Parse() to not fail, but got error : %v", err)
177213
}
178214

179-
if denylist.IsIncluded(item1) {
215+
isIncluded, err := denylist.IsIncluded(item1)
216+
if err != nil {
217+
t.Fatal("expected IsIncluded() to not fail")
218+
}
219+
if isIncluded {
180220
t.Fatal("expected excluded item to be excluded")
181221
}
182222
})
@@ -224,7 +264,8 @@ func TestStatus(t *testing.T) {
224264
allowlist, _ := New(map[string]struct{}{item1: {}, item2: {}}, map[string]struct{}{})
225265
actualStatusString := allowlist.Status()
226266
expectedRegexPattern := `^Including the following lists that were on allowlist: (item1|item2), (item2|item1)$`
227-
matched, _ := regexp.MatchString(expectedRegexPattern, actualStatusString)
267+
re := regexp.MustCompile(expectedRegexPattern, regexpDefaultSpec)
268+
matched, _ := re.MatchString(actualStatusString)
228269
if !matched {
229270
t.Errorf("expected status %q but got %q", expectedRegexPattern, actualStatusString)
230271
}
@@ -244,9 +285,38 @@ func TestStatus(t *testing.T) {
244285
denylist, _ := New(map[string]struct{}{}, map[string]struct{}{item1: {}, item2: {}})
245286
actualStatusString := denylist.Status()
246287
expectedRegexPattern := `^Excluding the following lists that were on denylist: (item1|item2), (item2|item1)$`
247-
matched, _ := regexp.MatchString(expectedRegexPattern, actualStatusString)
288+
re := regexp.MustCompile(expectedRegexPattern, regexpDefaultSpec)
289+
matched, _ := re.MatchString(actualStatusString)
248290
if !matched {
249291
t.Errorf("expected status %q but got %q", expectedRegexPattern, actualStatusString)
250292
}
251293
})
252294
}
295+
296+
func TestCatastrophicBacktrackTimeout(t *testing.T) {
297+
r, err := regexp.Compile("(.+)*\\?", 0)
298+
if err != nil {
299+
t.Fatal(err)
300+
}
301+
var exp = "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
302+
exp = strings.Repeat(exp, 2^10)
303+
304+
timeout := regexpDefaultTimeout
305+
t.Logf("regexp.DefaultMatchTimeout set to: %v", timeout)
306+
buffer := 500 * time.Millisecond
307+
t.Run(fmt.Sprint(timeout), func(t *testing.T) {
308+
r.MatchTimeout = timeout
309+
start := time.Now()
310+
_, err = r.FindStringMatch(exp)
311+
if err != nil && !strings.HasPrefix(err.Error(), "match timeout") {
312+
t.Fatal(err)
313+
}
314+
if err == nil {
315+
t.Fatal("expected catastrophic backtracking error")
316+
}
317+
elapsed := time.Since(start)
318+
if elapsed > timeout+buffer {
319+
t.Fatalf("timeout %v exceeded: %v", timeout, elapsed)
320+
}
321+
})
322+
}

pkg/options/options.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,8 @@ func (o *Options) AddFlags(cmd *cobra.Command) {
162162
o.cmd.Flags().StringVar((*string)(&o.Node), "node", "", "Name of the node that contains the kube-state-metrics pod. Most likely it should be passed via the downward API. This is used for daemonset sharding. Only available for resources (pod metrics) that support spec.nodeName fieldSelector. This is experimental.")
163163
o.cmd.Flags().Var(&o.AnnotationsAllowList, "metric-annotations-allowlist", "Comma-separated list of Kubernetes annotations keys that will be used in the resource' labels metric. By default the annotations metrics are not exposed. To include them, provide a list of resource names in their plural form and Kubernetes annotation keys you would like to allow for them (Example: '=namespaces=[kubernetes.io/team,...],pods=[kubernetes.io/team],...)'. A single '*' can be provided per resource instead to allow any annotations, but that has severe performance implications (Example: '=pods=[*]').")
164164
o.cmd.Flags().Var(&o.LabelsAllowList, "metric-labels-allowlist", "Comma-separated list of additional Kubernetes label keys that will be used in the resource' labels metric. By default the labels metrics are not exposed. To include them, provide a list of resource names in their plural form and Kubernetes label keys you would like to allow for them (Example: '=namespaces=[k8s-label-1,k8s-label-n,...],pods=[app],...)'. A single '*' can be provided per resource instead to allow any labels, but that has severe performance implications (Example: '=pods=[*]'). Additionally, an asterisk (*) can be provided as a key, which will resolve to all resources, i.e., assuming '--resources=deployments,pods', '=*=[*]' will resolve to '=deployments=[*],pods=[*]'.")
165-
o.cmd.Flags().Var(&o.MetricAllowlist, "metric-allowlist", "Comma-separated list of metrics to be exposed. This list comprises of exact metric names and/or regex patterns. The allowlist and denylist are mutually exclusive.")
166-
o.cmd.Flags().Var(&o.MetricDenylist, "metric-denylist", "Comma-separated list of metrics not to be enabled. This list comprises of exact metric names and/or regex patterns. The allowlist and denylist are mutually exclusive.")
165+
o.cmd.Flags().Var(&o.MetricAllowlist, "metric-allowlist", "Comma-separated list of metrics to be exposed. This list comprises of exact metric names and/or *ECMAScript-based* regex patterns. The allowlist and denylist are mutually exclusive.")
166+
o.cmd.Flags().Var(&o.MetricDenylist, "metric-denylist", "Comma-separated list of metrics not to be enabled. This list comprises of exact metric names and/or *ECMAScript-based* regex patterns. The allowlist and denylist are mutually exclusive.")
167167
o.cmd.Flags().Var(&o.MetricOptInList, "metric-opt-in-list", "Comma-separated list of metrics which are opt-in and not enabled by default. This is in addition to the metric allow- and denylists")
168168
o.cmd.Flags().Var(&o.Namespaces, "namespaces", fmt.Sprintf("Comma-separated list of namespaces to be enabled. Defaults to %q", &DefaultNamespaces))
169169
o.cmd.Flags().Var(&o.NamespacesDenylist, "namespaces-denylist", "Comma-separated list of namespaces not to be enabled. If namespaces and namespaces-denylist are both set, only namespaces that are excluded in namespaces-denylist will be used.")

0 commit comments

Comments
 (0)