Skip to content

Commit 65d9bb8

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

File tree

6 files changed

+124
-27
lines changed

6 files changed

+124
-27
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.23.0
55
require (
66
github.com/KimMachineGun/automemlimit v0.7.0
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
@@ -16,6 +16,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
1616
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1717
github.com/dgryski/go-jump v0.0.0-20211018200510-ba001c3ffce0 h1:0wH6nO9QEa02Qx8sIQGw6ieKdz+BXjpccSOo9vXNl4U=
1818
github.com/dgryski/go-jump v0.0.0-20211018200510-ba001c3ffce0/go.mod h1:4hKCXuwrJoYvHZxJ86+bRVTOMyJ0Ej+RqfSm8mHi6KA=
19+
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
20+
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
1921
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
2022
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
2123
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=

pkg/allowdenylist/allowdenylist.go

+35-11
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,19 @@ 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 regexpDefaultSpec regexp.RegexOptions = regexp.ECMAScript
32+
33+
// AllowDenyList namespaceencapsulates the logic needed to filter based on a string.
2834
type AllowDenyList struct {
2935
list map[string]struct{}
3036
rList []*regexp.Regexp
@@ -34,6 +40,7 @@ type AllowDenyList struct {
3440
// New constructs a new AllowDenyList based on a allow- and a
3541
// denylist. Only one of them can be not empty.
3642
func New(allow, deny map[string]struct{}) (*AllowDenyList, error) {
43+
regexp.DefaultMatchTimeout = time.Minute
3744
if len(allow) != 0 && len(deny) != 0 {
3845
return nil, errors.New(
3946
"allowlist and denylist are both set, they are mutually exclusive, only one of them can be set",
@@ -62,7 +69,7 @@ func New(allow, deny map[string]struct{}) (*AllowDenyList, error) {
6269
func (l *AllowDenyList) Parse() error {
6370
regexes := make([]*regexp.Regexp, 0, len(l.list))
6471
for item := range l.list {
65-
r, err := regexp.Compile(item)
72+
r, err := regexp.Compile(item, regexpDefaultSpec)
6673
if err != nil {
6774
return err
6875
}
@@ -99,25 +106,36 @@ func (l *AllowDenyList) Exclude(items []string) {
99106
}
100107

101108
// IsIncluded returns if the given item is included.
102-
func (l *AllowDenyList) IsIncluded(item string) bool {
103-
var matched bool
109+
func (l *AllowDenyList) IsIncluded(item string) (bool, error) {
110+
var (
111+
matched bool
112+
err error
113+
)
104114
for _, r := range l.rList {
105-
matched = r.MatchString(item)
115+
matched, err = r.MatchString(item)
116+
if err != nil {
117+
return false, err
118+
}
106119
if matched {
107120
break
108121
}
109122
}
110123

111124
if l.isAllowList {
112-
return matched
125+
return matched, nil
113126
}
114127

115-
return !matched
128+
return !matched, nil
116129
}
117130

118131
// IsExcluded returns if the given item is excluded.
119-
func (l *AllowDenyList) IsExcluded(item string) bool {
120-
return !l.IsIncluded(item)
132+
func (l *AllowDenyList) IsExcluded(item string) (bool, error) {
133+
isIncluded, err := l.IsIncluded(item)
134+
if err != nil {
135+
return false, err
136+
}
137+
138+
return !isIncluded, nil
121139
}
122140

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

138156
// Test returns if the given family generator passes (is included in) the AllowDenyList
139157
func (l *AllowDenyList) Test(generator generator.FamilyGenerator) bool {
140-
return l.IsIncluded(generator.Name)
158+
isIncluded, err := l.IsIncluded(generator.Name)
159+
if err != nil {
160+
klog.ErrorS(err, "Error while processing allow-deny entries for generator", "generator", generator.Name)
161+
return false
162+
}
163+
164+
return isIncluded
141165
}
142166

143167
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 := regexp.DefaultMatchTimeout
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

+1-1
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ func (o *Options) AddFlags(cmd *cobra.Command) {
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=[*]'.")
165165
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.")
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)