Skip to content

Commit 5f11691

Browse files
authored
Merge pull request #2616 from rexagod/2594
feat: Use `dlclark/regexp2` over standard library's package
2 parents bc60722 + 8b631bb commit 5f11691

File tree

8 files changed

+143
-29
lines changed

8 files changed

+143
-29
lines changed

README.md

+5
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ are deleted they are no longer visible on the `/metrics` endpoint.
4141
* [Resource group version compatibility](#resource-group-version-compatibility)
4242
* [Container Image](#container-image)
4343
* [Metrics Documentation](#metrics-documentation)
44+
* [ECMAScript regular expression support for allow and deny lists](#ecmascript-regular-expression-support-for-allow-and-deny-lists)
4445
* [Conflict resolution in label names](#conflict-resolution-in-label-names)
4546
* [Kube-state-metrics self metrics](#kube-state-metrics-self-metrics)
4647
* [Resource recommendation](#resource-recommendation)
@@ -129,6 +130,10 @@ e.g. by standardizing Kubernetes labels using an
129130
[Admission Webhook](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/)
130131
that ensures that there are no possible conflicts.
131132

133+
#### ECMAScript regular expression support for allow and deny lists
134+
135+
Starting from [#2616](https://github.com/kubernetes/kube-state-metrics/pull/2616/files), kube-state-metrics supports ECMAScript's `regexp` for allow and deny lists. This was incorporated as a workaround for the limitations of the `regexp` package in Go, which does not support lookarounds due to their non-linear time complexity. Please note that while lookarounds are now supported for allow and deny lists, regular expressions' evaluation time is capped at a minute to prevent performance issues.
136+
132137
### Kube-state-metrics self metrics
133138

134139
kube-state-metrics exposes its own general process metrics under `--telemetry-host` and `--telemetry-port` (default 8081).

README.md.tpl

+5
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ are deleted they are no longer visible on the `/metrics` endpoint.
4141
* [Resource group version compatibility](#resource-group-version-compatibility)
4242
* [Container Image](#container-image)
4343
* [Metrics Documentation](#metrics-documentation)
44+
* [ECMAScript regular expression support for allow and deny lists](#ecmascript-regular-expression-support-for-allow-and-deny-lists)
4445
* [Conflict resolution in label names](#conflict-resolution-in-label-names)
4546
* [Kube-state-metrics self metrics](#kube-state-metrics-self-metrics)
4647
* [Resource recommendation](#resource-recommendation)
@@ -130,6 +131,10 @@ e.g. by standardizing Kubernetes labels using an
130131
[Admission Webhook](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/)
131132
that ensures that there are no possible conflicts.
132133

134+
#### ECMAScript regular expression support for allow and deny lists
135+
136+
Starting from [#2616](https://github.com/kubernetes/kube-state-metrics/pull/2616/files), kube-state-metrics supports ECMAScript's `regexp` for allow and deny lists. This was incorporated as a workaround for the limitations of the `regexp` package in Go, which does not support lookarounds due to their non-linear time complexity. Please note that while lookarounds are now supported for allow and deny lists, regular expressions' evaluation time is capped at a minute to prevent performance issues.
137+
133138
### Kube-state-metrics self metrics
134139

135140
kube-state-metrics exposes its own general process metrics under `--telemetry-host` and `--telemetry-port` (default 8081).

docs/developer/cli-arguments.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@ Flags:
5656
--log_file string If non-empty, use this log file (no effect when -logtostderr=true)
5757
--log_file_max_size uint Defines the maximum size a log file can grow to (no effect when -logtostderr=true). Unit is megabytes. If the value is 0, the maximum file size is unlimited. (default 1800)
5858
--logtostderr log to standard error instead of files (default true)
59-
--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.
59+
--metric-allowlist string 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.
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

+42-11
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,24 @@ package allowdenylist
1818

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

2428
generator "k8s.io/kube-state-metrics/v2/pkg/metric_generator"
2529
)
2630

27-
// AllowDenyList encapsulates the logic needed to filter based on a string.
31+
// Use ECMAScript as the default regexp spec to support lookarounds (#2594).
32+
var (
33+
once sync.Once
34+
regexpDefaultSpec regexp.RegexOptions = regexp.ECMAScript
35+
regexpDefaultTimeout = time.Minute
36+
)
37+
38+
// AllowDenyList namespaceencapsulates the logic needed to filter based on a string.
2839
type AllowDenyList struct {
2940
list map[string]struct{}
3041
rList []*regexp.Regexp
@@ -34,6 +45,9 @@ type AllowDenyList struct {
3445
// New constructs a new AllowDenyList based on a allow- and a
3546
// denylist. Only one of them can be not empty.
3647
func New(allow, deny map[string]struct{}) (*AllowDenyList, error) {
48+
once.Do(func() {
49+
regexp.DefaultMatchTimeout = regexpDefaultTimeout
50+
})
3751
if len(allow) != 0 && len(deny) != 0 {
3852
return nil, errors.New(
3953
"allowlist and denylist are both set, they are mutually exclusive, only one of them can be set",
@@ -62,7 +76,7 @@ func New(allow, deny map[string]struct{}) (*AllowDenyList, error) {
6276
func (l *AllowDenyList) Parse() error {
6377
regexes := make([]*regexp.Regexp, 0, len(l.list))
6478
for item := range l.list {
65-
r, err := regexp.Compile(item)
79+
r, err := regexp.Compile(item, regexpDefaultSpec)
6680
if err != nil {
6781
return err
6882
}
@@ -99,25 +113,36 @@ func (l *AllowDenyList) Exclude(items []string) {
99113
}
100114

101115
// IsIncluded returns if the given item is included.
102-
func (l *AllowDenyList) IsIncluded(item string) bool {
103-
var matched bool
116+
func (l *AllowDenyList) IsIncluded(item string) (bool, error) {
117+
var (
118+
matched bool
119+
err error
120+
)
104121
for _, r := range l.rList {
105-
matched = r.MatchString(item)
122+
matched, err = r.MatchString(item)
123+
if err != nil {
124+
return false, err
125+
}
106126
if matched {
107127
break
108128
}
109129
}
110130

111131
if l.isAllowList {
112-
return matched
132+
return matched, nil
113133
}
114134

115-
return !matched
135+
return !matched, nil
116136
}
117137

118138
// IsExcluded returns if the given item is excluded.
119-
func (l *AllowDenyList) IsExcluded(item string) bool {
120-
return !l.IsIncluded(item)
139+
func (l *AllowDenyList) IsExcluded(item string) (bool, error) {
140+
isIncluded, err := l.IsIncluded(item)
141+
if err != nil {
142+
return false, err
143+
}
144+
145+
return !isIncluded, nil
121146
}
122147

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

138163
// Test returns if the given family generator passes (is included in) the AllowDenyList
139164
func (l *AllowDenyList) Test(generator generator.FamilyGenerator) bool {
140-
return l.IsIncluded(generator.Name)
165+
isIncluded, err := l.IsIncluded(generator.Name)
166+
if err != nil {
167+
klog.ErrorS(err, "Error while processing allow-deny entries for generator", "generator", generator.Name)
168+
return false
169+
}
170+
171+
return isIncluded
141172
}
142173

143174
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)