Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
* [ENHANCEMENT] Add de-duplicated chunks counter `cortex_chunk_store_deduped_chunks_total` which counts every chunk not sent to the store because it was already sent by another replica. #2485
* [ENHANCEMENT] query-frontend now also logs the POST data of long queries. #2481
* [ENHANCEMENT] Experimental WAL: Ingester WAL records now have type header and the custom WAL records have been replaced by Prometheus TSDB's WAL records. Old records will not be supported from 1.3 onwards. Note: once this is deployed, you cannot downgrade without data loss. #2436
* [BUGFIX] Ruler: Ensure temporary rule files with special characters are properly mapped and cleaned up. #2506
* [BUGFIX] Fixes #2411, Ensure requests are properly routed to the prometheus api embedded in the query if `-server.path-prefix` is set. #2372
* [BUGFIX] Experimental TSDB: fixed chunk data corruption when querying back series using the experimental blocks storage. #2400
* [BUGFIX] Cassandra Storage: Fix endpoint TLS host verification. #2109
Expand Down
56 changes: 56 additions & 0 deletions pkg/ruler/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,62 @@ func TestRuler_rules(t *testing.T) {
require.Equal(t, string(expectedResponse), string(body))
}

func TestRuler_rules_special_characters(t *testing.T) {
cfg, cleanup := defaultRulerConfig(newMockRuleStore(mockSpecialCharRules))
defer cleanup()

r, rcleanup := newTestRuler(t, cfg)
defer rcleanup()
defer services.StopAndAwaitTerminated(context.Background(), r) //nolint:errcheck

req := httptest.NewRequest("GET", "https://localhost:8080/api/prom/api/v1/rules", nil)
req.Header.Add(user.OrgIDHeaderName, "user1")
w := httptest.NewRecorder()
r.PrometheusRules(w, req)

resp := w.Result()
body, _ := ioutil.ReadAll(resp.Body)

// Check status code and status response
responseJSON := response{}
err := json.Unmarshal(body, &responseJSON)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
require.Equal(t, responseJSON.Status, "success")

// Testing the running rules for user1 in the mock store
expectedResponse, _ := json.Marshal(response{
Status: "success",
Data: &RuleDiscovery{
RuleGroups: []*RuleGroup{
{
Name: ")(_+?/|group1+/?",
File: ")(_+?/|namespace1+/?",
Rules: []rule{
&recordingRule{
Name: "UP_RULE",
Query: "up",
Health: "unknown",
Type: "recording",
},
&alertingRule{
Name: "UP_ALERT",
Query: "up < 1",
State: "inactive",
Health: "unknown",
Type: "alerting",
Alerts: []*Alert{},
},
},
Interval: 60,
},
},
},
})

require.Equal(t, string(expectedResponse), string(body))
}

func TestRuler_alerts(t *testing.T) {
cfg, cleanup := defaultRulerConfig(newMockRuleStore(mockRules))
defer cleanup()
Expand Down
22 changes: 17 additions & 5 deletions pkg/ruler/mapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package ruler

import (
"crypto/md5"
"net/url"
"path/filepath"
"sort"

"github.com/go-kit/kit/log"
Expand Down Expand Up @@ -33,16 +35,18 @@ func (m *mapper) MapRules(user string, ruleConfigs map[string][]legacy_rulefmt.R
anyUpdated := false
filenames := []string{}

// user rule files will be stored as `/<path>/<userid>/filename`
path := m.Path + "/" + user + "/"
// user rule files will be stored as `/<path>/<userid>/<encoded filename>`
path := filepath.Join(m.Path, user)
err := m.FS.MkdirAll(path, 0777)
if err != nil {
return false, nil, err
}

// write all rule configs to disk
for filename, groups := range ruleConfigs {
fullFileName := path + filename
// Store the encoded file name to better handle `/` characters
encodedFileName := url.PathEscape(filename)
fullFileName := filepath.Join(path, encodedFileName)

fileUpdated, err := m.writeRuleGroupsIfNewer(groups, fullFileName)
if err != nil {
Expand All @@ -59,8 +63,16 @@ func (m *mapper) MapRules(user string, ruleConfigs map[string][]legacy_rulefmt.R
}

for _, existingFile := range existingFiles {
fullFileName := path + existingFile.Name()
ruleGroups := ruleConfigs[existingFile.Name()]
fullFileName := filepath.Join(path, existingFile.Name())

// Ensure the namespace is decoded from a url path encoding to see if it is still required
decodedNamespace, err := url.PathUnescape(existingFile.Name())
if err != nil {
level.Warn(m.logger).Log("msg", "unable to remove rule file on disk", "file", fullFileName, "err", err)
continue
}

ruleGroups := ruleConfigs[string(decodedNamespace)]

if ruleGroups == nil {
err = m.FS.Remove(fullFileName)
Expand Down
114 changes: 87 additions & 27 deletions pkg/ruler/mapper_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ruler

import (
"net/url"
"os"
"testing"

Expand All @@ -15,8 +16,14 @@ import (
var (
testUser = "user1"

fileOneEncoded = url.PathEscape("file /one")
fileTwoEncoded = url.PathEscape("file /two")

fileOnePath = "/rules/user1/" + fileOneEncoded
fileTwoPath = "/rules/user1/" + fileTwoEncoded

initialRuleSet = map[string][]legacy_rulefmt.RuleGroup{
"file_one": {
"file /one": {
{
Name: "rulegroup_one",
Rules: []legacy_rulefmt.Rule{
Expand All @@ -39,7 +46,7 @@ var (
}

outOfOrderRuleSet = map[string][]legacy_rulefmt.RuleGroup{
"file_one": {
"file /one": {
{
Name: "rulegroup_two",
Rules: []legacy_rulefmt.Rule{
Expand All @@ -62,7 +69,7 @@ var (
}

updatedRuleSet = map[string][]legacy_rulefmt.RuleGroup{
"file_one": {
"file /one": {
{
Name: "rulegroup_one",
Rules: []legacy_rulefmt.Rule{
Expand Down Expand Up @@ -107,10 +114,10 @@ func Test_mapper_MapRules(t *testing.T) {
updated, files, err := m.MapRules(testUser, initialRuleSet)
require.True(t, updated)
require.Len(t, files, 1)
require.Equal(t, "/rules/user1/file_one", files[0])
require.Equal(t, fileOnePath, files[0])
require.NoError(t, err)

exists, err := afero.Exists(m.FS, "/rules/user1/file_one")
exists, err := afero.Exists(m.FS, fileOnePath)
require.True(t, exists)
require.NoError(t, err)
})
Expand All @@ -121,7 +128,7 @@ func Test_mapper_MapRules(t *testing.T) {
require.Len(t, files, 1)
require.NoError(t, err)

exists, err := afero.Exists(m.FS, "/rules/user1/file_one")
exists, err := afero.Exists(m.FS, fileOnePath)
require.True(t, exists)
require.NoError(t, err)
})
Expand All @@ -132,7 +139,7 @@ func Test_mapper_MapRules(t *testing.T) {
require.Len(t, files, 1)
require.NoError(t, err)

exists, err := afero.Exists(m.FS, "/rules/user1/file_one")
exists, err := afero.Exists(m.FS, fileOnePath)
require.True(t, exists)
require.NoError(t, err)
})
Expand All @@ -141,18 +148,18 @@ func Test_mapper_MapRules(t *testing.T) {
updated, files, err := m.MapRules(testUser, updatedRuleSet)
require.True(t, updated)
require.Len(t, files, 1)
require.Equal(t, "/rules/user1/file_one", files[0])
require.Equal(t, fileOnePath, files[0])
require.NoError(t, err)

exists, err := afero.Exists(m.FS, "/rules/user1/file_one")
exists, err := afero.Exists(m.FS, fileOnePath)
require.True(t, exists)
require.NoError(t, err)
})
}

var (
twoFilesRuleSet = map[string][]legacy_rulefmt.RuleGroup{
"file_one": {
"file /one": {
{
Name: "rulegroup_one",
Rules: []legacy_rulefmt.Rule{
Expand All @@ -172,7 +179,7 @@ var (
},
},
},
"file_two": {
"file /two": {
{
Name: "rulegroup_one",
Rules: []legacy_rulefmt.Rule{
Expand All @@ -186,7 +193,7 @@ var (
}

twoFilesUpdatedRuleSet = map[string][]legacy_rulefmt.RuleGroup{
"file_one": {
"file /one": {
{
Name: "rulegroup_one",
Rules: []legacy_rulefmt.Rule{
Expand All @@ -206,7 +213,7 @@ var (
},
},
},
"file_two": {
"file /two": {
{
Name: "rulegroup_one",
Rules: []legacy_rulefmt.Rule{
Expand All @@ -220,7 +227,7 @@ var (
}

twoFilesDeletedRuleSet = map[string][]legacy_rulefmt.RuleGroup{
"file_one": {
"file /one": {
{
Name: "rulegroup_one",
Rules: []legacy_rulefmt.Rule{
Expand Down Expand Up @@ -256,10 +263,10 @@ func Test_mapper_MapRulesMultipleFiles(t *testing.T) {
updated, files, err := m.MapRules(testUser, initialRuleSet)
require.True(t, updated)
require.Len(t, files, 1)
require.Equal(t, "/rules/user1/file_one", files[0])
require.Equal(t, fileOnePath, files[0])
require.NoError(t, err)

exists, err := afero.Exists(m.FS, "/rules/user1/file_one")
exists, err := afero.Exists(m.FS, fileOnePath)
require.True(t, exists)
require.NoError(t, err)
})
Expand All @@ -268,14 +275,14 @@ func Test_mapper_MapRulesMultipleFiles(t *testing.T) {
updated, files, err := m.MapRules(testUser, twoFilesRuleSet)
require.True(t, updated)
require.Len(t, files, 2)
require.True(t, sliceContains(t, "/rules/user1/file_one", files))
require.True(t, sliceContains(t, "/rules/user1/file_two", files))
require.True(t, sliceContains(t, fileOnePath, files))
require.True(t, sliceContains(t, fileTwoPath, files))
require.NoError(t, err)

exists, err := afero.Exists(m.FS, "/rules/user1/file_one")
exists, err := afero.Exists(m.FS, fileOnePath)
require.True(t, exists)
require.NoError(t, err)
exists, err = afero.Exists(m.FS, "/rules/user1/file_two")
exists, err = afero.Exists(m.FS, fileTwoPath)
require.True(t, exists)
require.NoError(t, err)
})
Expand All @@ -284,14 +291,14 @@ func Test_mapper_MapRulesMultipleFiles(t *testing.T) {
updated, files, err := m.MapRules(testUser, twoFilesUpdatedRuleSet)
require.True(t, updated)
require.Len(t, files, 2)
require.True(t, sliceContains(t, "/rules/user1/file_one", files))
require.True(t, sliceContains(t, "/rules/user1/file_two", files))
require.True(t, sliceContains(t, fileOnePath, files))
require.True(t, sliceContains(t, fileTwoPath, files))
require.NoError(t, err)

exists, err := afero.Exists(m.FS, "/rules/user1/file_one")
exists, err := afero.Exists(m.FS, fileOnePath)
require.True(t, exists)
require.NoError(t, err)
exists, err = afero.Exists(m.FS, "/rules/user1/file_two")
exists, err = afero.Exists(m.FS, fileTwoPath)
require.True(t, exists)
require.NoError(t, err)
})
Expand All @@ -300,19 +307,72 @@ func Test_mapper_MapRulesMultipleFiles(t *testing.T) {
updated, files, err := m.MapRules(testUser, twoFilesDeletedRuleSet)
require.True(t, updated)
require.Len(t, files, 1)
require.Equal(t, "/rules/user1/file_one", files[0])
require.Equal(t, fileOnePath, files[0])
require.NoError(t, err)

exists, err := afero.Exists(m.FS, "/rules/user1/file_one")
exists, err := afero.Exists(m.FS, fileOnePath)
require.True(t, exists)
require.NoError(t, err)
exists, err = afero.Exists(m.FS, "/rules/user1/file_two")
exists, err = afero.Exists(m.FS, fileTwoPath)
require.False(t, exists)
require.NoError(t, err)
})

}

var (
specialCharFile = "+A_/ReallyStrange<>NAME:SPACE/?"
specialCharFileEncoded = url.PathEscape(specialCharFile)
specialCharFilePath = "/rules/user1/" + specialCharFileEncoded

specialCharactersRuleSet = map[string][]legacy_rulefmt.RuleGroup{
specialCharFile: {
{
Name: "rulegroup_one",
Rules: []legacy_rulefmt.Rule{
{
Record: "example_rule",
Expr: "example_expr",
},
},
},
},
}
)

func Test_mapper_MapRulesSpecialCharNamespace(t *testing.T) {
l := log.NewLogfmtLogger(os.Stdout)
l = level.NewFilter(l, level.AllowInfo())
m := &mapper{
Path: "/rules",
FS: afero.NewMemMapFs(),
logger: l,
}

t.Run("create special characters rulegroup", func(t *testing.T) {
updated, files, err := m.MapRules(testUser, specialCharactersRuleSet)
require.NoError(t, err)
require.True(t, updated)
require.Len(t, files, 1)
require.Equal(t, specialCharFilePath, files[0])

exists, err := afero.Exists(m.FS, specialCharFilePath)
require.NoError(t, err)
require.True(t, exists)
})

t.Run("delete special characters rulegroup", func(t *testing.T) {
updated, files, err := m.MapRules(testUser, map[string][]legacy_rulefmt.RuleGroup{})
require.NoError(t, err)
require.True(t, updated)
require.Len(t, files, 0)

exists, err := afero.Exists(m.FS, specialCharFilePath)
require.NoError(t, err)
require.False(t, exists)
})
}

func sliceContains(t *testing.T, find string, in []string) bool {
t.Helper()

Expand Down
9 changes: 8 additions & 1 deletion pkg/ruler/ruler.go
Original file line number Diff line number Diff line change
Expand Up @@ -530,10 +530,17 @@ func (r *Ruler) getLocalRules(userID string) ([]*GroupStateDesc, error) {

for _, group := range groups {
interval := group.Interval()

// The mapped filename is url path escaped encoded to make handling `/` characters easier
decodedNamespace, err := url.PathUnescape(strings.TrimPrefix(group.File(), prefix))
if err != nil {
return nil, errors.Wrap(err, "unable to decode rule filename")
}

groupDesc := &GroupStateDesc{
Group: &rules.RuleGroupDesc{
Name: group.Name(),
Namespace: strings.TrimPrefix(group.File(), prefix),
Namespace: string(decodedNamespace),
Interval: interval,
User: userID,
},
Expand Down
Loading