Repository: thanos-io/thanos (Go)

Issue:
rules-check doesn't accept globs as described in the documentation
The example rules-check command states that rules-check takes a glob of rules files
```
./thanos tools rules-check --rules cmd/thanos/testdata/rules-files/*.yaml
```
https://thanos.io/v0.19/components/tools.md/#rules-check
But this does not work for me.

You either get shell expansion of the glob, resulting in:
```
./thanos tools rules-check --rules config/rules/*.yaml
error parsing commandline arguments: [/bin/thanos tools rules-check --rules config/rules/file1.yaml config/rules/file2.yaml]: unexpected config/rules/file2.yaml
thanos: error: unexpected config/rules/file2.yaml
```

Or without shell expansion:
```
./thanos tools rules-check --rules=config/rules/*.yaml
error parsing commandline arguments: [/bin/thanos tools rules-check --rules=config/rules/*.yaml]: path 'config/rules/*.yaml' does not exist
usage: thanos tools rules-check --rules=RULES
```

The only form that does work is
```
./thanos tools rules-check --rules config/rules/file1.yaml --rules config/rules/file2.yaml
```

This is either a documentation error or the glob handling is not working.


Discussion:
It seems like we use `ExistingFiles()` which allows you to specify multiple files that exist but it doesn't support regular expressions. IMHO we should add support for that. Help wanted!
Which version is this fixed in? I just ran into it in both `v0.20.1` and `v0.20.2`.



Patch:
diff --git a/cmd/thanos/tools.go b/cmd/thanos/tools.go
index c0c9b04ed8d..e916da8ed0b 100644
--- a/cmd/thanos/tools.go
+++ b/cmd/thanos/tools.go
@@ -5,11 +5,13 @@ package main
 
 import (
 	"os"
+	"path/filepath"
 
 	"github.com/go-kit/kit/log"
 	"github.com/go-kit/kit/log/level"
 	"github.com/oklog/run"
 	"github.com/opentracing/opentracing-go"
+	"github.com/pkg/errors"
 	"github.com/prometheus/client_golang/prometheus"
 	"github.com/thanos-io/thanos/pkg/errutil"
 	"github.com/thanos-io/thanos/pkg/extkingpin"
@@ -25,7 +27,7 @@ func registerTools(app *extkingpin.App) {
 
 func registerCheckRules(app extkingpin.AppClause) {
 	cmd := app.Command("rules-check", "Check if the rule files are valid or not.")
-	ruleFiles := cmd.Flag("rules", "The rule files glob to check (repeated).").Required().ExistingFiles()
+	ruleFiles := cmd.Flag("rules", "The rule files glob to check (repeated).").Required().Strings()
 
 	cmd.Setup(func(g *run.Group, logger log.Logger, reg *prometheus.Registry, _ opentracing.Tracer, _ <-chan struct{}, _ bool) error {
 		// Dummy actor to immediately kill the group after the run function returns.
@@ -34,31 +36,42 @@ func registerCheckRules(app extkingpin.AppClause) {
 	})
 }
 
-func checkRulesFiles(logger log.Logger, files *[]string) error {
+func checkRulesFiles(logger log.Logger, patterns *[]string) error {
 	var failed errutil.MultiError
 
-	for _, fn := range *files {
-		level.Info(logger).Log("msg", "checking", "filename", fn)
-		f, err := os.Open(fn)
-		if err != nil {
+	for _, p := range *patterns {
+		level.Info(logger).Log("msg", "checking", "pattern", p)
+		matches, err := filepath.Glob(p)
+		if err != nil || matches == nil {
+			err = errors.New("matching file not found")
 			level.Error(logger).Log("result", "FAILED", "error", err)
 			level.Info(logger).Log()
 			failed.Add(err)
 			continue
 		}
-		defer func() { _ = f.Close() }()
+		for _, fn := range matches {
+			level.Info(logger).Log("msg", "checking", "filename", fn)
+			f, er := os.Open(fn)
+			if er != nil {
+				level.Error(logger).Log("result", "FAILED", "error", er)
+				level.Info(logger).Log()
+				failed.Add(err)
+				continue
+			}
+			defer func() { _ = f.Close() }()
 
-		n, errs := rules.ValidateAndCount(f)
-		if errs.Err() != nil {
-			level.Error(logger).Log("result", "FAILED")
-			for _, e := range errs {
-				level.Error(logger).Log("error", e.Error())
-				failed.Add(e)
+			n, errs := rules.ValidateAndCount(f)
+			if errs.Err() != nil {
+				level.Error(logger).Log("result", "FAILED")
+				for _, e := range errs {
+					level.Error(logger).Log("error", e.Error())
+					failed.Add(e)
+				}
+				level.Info(logger).Log()
+				continue
 			}
-			level.Info(logger).Log()
-			continue
+			level.Info(logger).Log("result", "SUCCESS", "rules found", n)
 		}
-		level.Info(logger).Log("result", "SUCCESS", "rules found", n)
 	}
 	return failed.Err()
 }


Test patch:
diff --git a/cmd/thanos/testdata/rules-files/valid_1.yaml b/cmd/thanos/testdata/rules-files/valid_1.yaml
new file mode 100644
index 00000000000..fb75e799e23
--- /dev/null
+++ b/cmd/thanos/testdata/rules-files/valid_1.yaml
@@ -0,0 +1,18 @@
+groups:
+  - name: test-alert-group
+    partial_response_strategy: "warn"
+    interval: 2m
+    rules:
+      - alert: TestAlert
+        expr: 1
+        labels:
+          key: value
+        annotations:
+          key: value
+
+  - name: test-rule-group
+    partial_response_strategy: "warn"
+    interval: 2m
+    rules:
+      - record: test_metric
+        expr: 1
diff --git a/cmd/thanos/testdata/rules-files/valid_2.yaml b/cmd/thanos/testdata/rules-files/valid_2.yaml
new file mode 100644
index 00000000000..fb75e799e23
--- /dev/null
+++ b/cmd/thanos/testdata/rules-files/valid_2.yaml
@@ -0,0 +1,18 @@
+groups:
+  - name: test-alert-group
+    partial_response_strategy: "warn"
+    interval: 2m
+    rules:
+      - alert: TestAlert
+        expr: 1
+        labels:
+          key: value
+        annotations:
+          key: value
+
+  - name: test-rule-group
+    partial_response_strategy: "warn"
+    interval: 2m
+    rules:
+      - record: test_metric
+        expr: 1
diff --git a/cmd/thanos/tools_test.go b/cmd/thanos/tools_test.go
index 8a5792f1804..512f788c905 100644
--- a/cmd/thanos/tools_test.go
+++ b/cmd/thanos/tools_test.go
@@ -29,3 +29,18 @@ func Test_CheckRules(t *testing.T) {
 		testutil.NotOk(t, checkRulesFiles(logger, &fn), "expected err for file %s", fn)
 	}
 }
+
+func Test_CheckRules_Glob(t *testing.T) {
+	// regex path
+	files := &[]string{"./testdata/rules-files/valid*.yaml"}
+	logger := log.NewNopLogger()
+	testutil.Ok(t, checkRulesFiles(logger, files))
+
+	// direct path
+	files = &[]string{"./testdata/rules-files/valid.yaml"}
+	testutil.Ok(t, checkRulesFiles(logger, files))
+
+	// invalid path
+	files = &[]string{"./testdata/rules-files/*.yamlaaa"}
+	testutil.NotOk(t, checkRulesFiles(logger, files), "expected err for file %s", files)
+}